【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 <bits/stdc++.h>
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 <bits/stdc++.h>
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 <bits/stdc++.h>
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 <bits/stdc++.h>
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<b.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\),期末考试结束当天夜晚。夜半狂欢

posted @ 2025-02-07 14:54  w9095  阅读(315)  评论(0)    收藏  举报