【6】欧拉路径学习笔记

【6】欧拉路径学习笔记

前言

欧拉路径和一笔画问题有关,问题的本质也是奇点的数量。不算很难,而且考的也不多,主要还是在第一轮考。

欧拉路径

如果在一个图中,存在由 \(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\),期末考试结束当天夜晚。夜半狂欢

相关推荐

东品游戏旗下产品
365防伪码查询系统

东品游戏旗下产品

📅 08-11 👁️ 3087
仁王2守护灵选择指南 哪个守护灵最适合你
365体育比分官网

仁王2守护灵选择指南 哪个守护灵最适合你

📅 06-28 👁️ 5176
我的前半生在哪个台播出
365体育比分官网

我的前半生在哪个台播出

📅 07-13 👁️ 1190