前言
欧拉路径和一笔画问题有关,问题的本质也是奇点的数量。不算很难,而且考的也不多,主要还是在第一轮考。
欧拉路径
如果在一个图中,存在由 \(i\) 点出发,把每条边遍历一次,最后到达 \(j\) 的一条路径,则这个图中存在欧拉路径。这条路径叫做欧拉路径。
若最后回到起点,即 \(i=j\),那么这条路径叫做欧拉回路。
无向图欧拉路径判定定理
奇点:与这个点相连的边数为奇数的点叫做奇点。
定理 \(1\):若图为连通图,且图中不存在奇点,则图中存在欧拉回路。
证明:因为每个点的度数均为偶数,所以每次到达这个点,必然经过一条连边,剩下的边数必然为奇数。因为 \(0\) 不是奇数,所以必然有一条未经过的边可以离开。离开之后,必然又会经过一条连边,剩下的边数必然为偶数,可以重复这个过程,直到未经过的边数为 \(0\)。
定理 \(2\):若图为连通图,且图中仅存在 \(2\) 个奇点,则图中存在欧拉路径。
证明:任意找到一个欧拉回路,删去回路中的一条边,那么会导致出现两个奇点。由于本来图是可以走回起点的,且每一条边必定被走过,那么从一个奇点出发,一定能走过剩下的所有边到达另一个奇点,存在欧拉路径。
另外,顺便提一下,一个无向图中不可能存在奇数个奇点。因为每增加一条边,奇点数量 \(+2\) 或 \(-2\),初始奇点数量又是 \(0\),是个偶数,所以不可能存在奇数个奇点。
未判连通代码如下:
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
d[u]++;d[v]++;
}
for(int i=1;i<=n;i++)
if(d[i]%2)flag=0;
printf("%d\n",flag);
有向图欧拉路径判定定理
记录每个点的入度和出度,记作 \(ind_i\) 和 \(oud_i\)。如果 \(\mid ind_i-oud_i\mid=1\),则这个点相当于无向图中的奇点。而如果存在 \(\mid ind_i-oud_i\mid\ge2\) 的点,则图中一定不存在欧拉路径。
未判连通代码如下:
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
oud[u]++;ind[v]++;
}
for(int i=1;i<=n;i++)
flag+=fabs(cd[i]-rd[i]);
if(flag<=2)printf("1\n");
else printf("0\n");
求法(Hierholzer 算法)
\(1\):从起点开始 DFS,删除选过的边。
\(2\):当前点不存在出边时回退,并将当前点入栈。
\(3\):DFS 结束时,倒序输出栈中的节点即可。
正确性证明:该算法其实就是每一次从欧拉路径中取走一个环,而环中每一个点的度数肯定为偶数,不会影响结果。
时间复杂度为 \(O(m)\),常用。
如果题目要求字典序最小,那么我们可以优先遍历编号较小的节点,最后倒序输出,就是字典序最小的路径。
void dfs(int now)
{
for(int i=1;i<=n;i++)
if(i!=now&&f[now][i])
{
f[now][i]--,f[i][now]--;
dfs(i);
}
lu[++cnt]=now;
}
例题
例题 \(1\):
【一本通提高篇欧拉回路】欧拉回路1
(站外题,提供题面展示)
欧拉回路的判定的模板题,注意用并查集来判定图是否连通,不多赘述。
#include
using namespace std;
long long n,m,d[600060],f[600060];
int getf(int x)
{
if(f[x]==x)return x;
else return f[x]=getf(f[x]);
}
void merge(int x,int y)
{
int p=getf(x),q=getf(y);
if(p!=q)f[q]=p;
}
bool query(int x,int y)
{
int p=getf(x),q=getf(y);
return p==q;
}
int main()
{
while(scanf("%d",&n)!=-1)
{
int flag=1;
if(n==0)break;
scanf("%d",&m);
for(int i=1;i<=n;i++)f[i]=i;
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
d[u]++;d[v]++;
merge(u,v);
}
for(int i=1;i<=n;i++)
if(!query(1,i))flag=0;
for(int i=1;i<=n;i++)
if(d[i]%2)flag=0;
printf("%d\n",flag);
memset(d,0,sizeof(d));
}
return 0;
}
例题 \(2\):
P2731 [USACO3.3] 骑马修栅栏 Riding the Fences
欧拉回路路径输出板子题,注意字典序最小,不多赘述。
#include
using namespace std;
int n,m,d[600],f[600][600],lu[60000],cnt=0;
void dfs(int now)
{
for(int i=1;i<=n;i++)
if(i!=now&&f[now][i])
{
f[now][i]--,f[i][now]--;
dfs(i);
}
lu[++cnt]=now;
}
int main()
{
scanf("%d",&m);
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
n=max(n,max(u,v));
d[u]++;d[v]++;
f[u][v]++,f[v][u]++;
}
for(int i=1;i<=n;i++)
if(d[i]%2)
{
dfs(i);
for(int i=cnt;i>0;i--)
printf("%d\n",lu[i]);
return 0;
}
dfs(1);
for(int i=cnt;i>0;i--)
printf("%d\n",lu[i]);
return 0;
}
例题 \(3\):
UVA10129 单词 Play on Words
考虑记录每个单词开头的字母 \(st\) 和结尾的字母 \(en\),建立一条 \(st\to en\) 的单向边,表示通过接龙这个单词,让最后一个字母从 \(st\) 变成了 \(en\)。
由于每个单词都需要用一次,也只能用一次,所以在图中相当于每条边只能走一次,且必须走一次,发现就是欧拉路径,直接用有向图欧拉路径判定定理解决。代码中的实现有一些繁琐。
#include
using namespace std;
int t,n,rd[26],cd[26],book[26],f[26];
char str[20000];
int getf(int x)
{
if(f[x]==x)return x;
else return f[x]=getf(f[x]);
}
void merge(int x,int y)
{
int p=getf(x),q=getf(y);
if(p!=q)f[q]=p;
}
bool query(int x,int y)
{
int p=getf(x),q=getf(y);
return p==q;
}
int main()
{
scanf("%d",&t);
while(t--)
{
int flag=0;
scanf("%d",&n);
for(int i=0;i<26;i++)f[i]=i;
for(int i=1;i<=n;i++)
{
scanf("%s",str);
int l=strlen(str);
int bh=str[0]-'a',bt=str[l-1]-'a';
book[bh]=book[bt]=1;
cd[bh]++;rd[bt]++;
merge(bh,bt);
}
for(int i=0;i<26;i++)
if(cd[i]!=rd[i])flag+=fabs(cd[i]-rd[i]);
for(int i=0;i<26;i++)
for(int j=0;j<26;j++)
if(book[i]&&book[j]&&!query(i,j))flag=99999999;
if(flag<=2)printf("Ordering is possible.\n");
else printf("The door cannot be opened.\n");
memset(rd,0,sizeof(rd));memset(cd,0,sizeof(cd));memset(book,0,sizeof(book));
}
return 0;
}
例题 \(4\):
UVA302 John's trip
我的题解
给定一张图和起点,求从起点开始能否把每条边遍历一次后返回起点,并输出字典序最小的路径。
欧拉回路裸题,若可以达到题目的要求,则图中必然存在一条欧拉回路。我们可以记录每个点的连的边数,如果出现一个点连的边数为奇数,那么图中必定不存在欧拉回路,输出 Round trip does not exist. 即可。原因是如果存在欧拉回路,那么回到起点前,走到每个点时必定还有一条路离开。所以走到一个点是一条路,离开一个点又是一条路,合起来就是两条路。如果一个点被经过两次,那么就是四条路。易得欧拉回路中每个点连的边数为偶数。
然后,对于存在欧拉回路的情况,我们可以从起点出发进行 DFS。由于题目要求输出字典序最小的路径,所以我们先遍历编号小的边。参照输出经过的点的路径的做法,当一条边遍历完成之后,将其压入栈中,最后倒序输出。
注意,这里有一个小坑点:如果一条边在遍历中走过了,由于每一条边只能经过一次,所以就不能再走了,这点和输出经过的点的路径不一样。所以,在走边都时候要注意判断这条边已经走过没有,否则会得到错误结果。
另外,再重复一遍翻译里的话:每次输出完额外换一行!
#include
using namespace std;
struct egde
{
int v,next,d;
}e[30000];
struct edgein
{
int d,t;
};
int n,m,b,d[30000],h[30000],lu[60000],book[30000],cnt=0,tol=0;
bool cmp(struct edgein a,struct edgein b)
{
return a.d } void add_edge(int u,int v,int d) { e[++tol].next=h[u]; e[tol].v=v; e[tol].d=d; h[u]=tol; } void dfs(int now) { int cn=0; struct edgein s[2000]; for(int i=h[now];i;i=e[i].next) if(!book[e[i].d])s[++cn].d=e[i].d,s[cn].t=e[i].v; if(cn==0)return; sort(s+1,s+cn+1,cmp); for(int i=1;i<=cn;i++) if(!book[s[i].d]) { int k=s[i].d; book[s[i].d]=1; dfs(s[i].t); lu[++cnt]=k; } } int main() { int u=0,v=0; while(scanf("%d%d",&u,&v)!=-1) { if(u==0&&v==0)break; int flag1=1,flag2=0,flag3=0; scanf("%d",&b); n=max(n,max(u,v));m=min(u,v); d[u]++;d[v]++; add_edge(u,v,b);add_edge(v,u,b); while(scanf("%d%d",&u,&v)!=-1) { if(u==0&&v==0)break; scanf("%d",&b); n=max(n,max(u,v)); d[u]++;d[v]++; add_edge(u,v,b);add_edge(v,u,b); } for(int i=1;i<=n;i++) { if(d[i]%2)flag3=1,flag1++; else if(d[i]%2&&i==m)flag2=1; } flag1=(flag1<=2)&&(!(flag2^flag3)); if(flag1) { dfs(m); for(int i=cnt;i>0;i--) if(i!=1)printf("%d ",lu[i]); else printf("%d",lu[i]); printf("\n\n"); } else printf("Round trip does not exist.\n\n"); cnt=0;tol=0; memset(h,0,sizeof(h));memset(lu,0,sizeof(lu));memset(d,0,sizeof(d)); memset(book,0,sizeof(book)); } return 0; } 后记 完成于\(2023.6.29\),午夜 \(23:23\),期末考试结束当天夜晚。夜半狂欢