Leaf-Salix.github.io

my blog

View on GitHub

【学习笔记】欧拉回路

前言

震惊!某位正在准备JXOI2019的蒟蒻竟然连欧拉回路都不会

于是他就开始学

参考博客

定义

度数:有向图中,出度和入度分别指一个点的出边和入边的条数;无向图中则指一个点所连边数

奇点:度数为奇数的点

欧拉路径:如果图G中的一个路径包括每一条边恰好一次(每条边经过一次),则该路径为欧拉路径

欧拉回路:一条欧拉路径的起点和终点为同一点;或理解为一个回路为欧拉路径则这条路径为欧拉回路

欧拉图:有欧拉回路的图

判断(充要条件)

无向图存在欧拉路径:除两个奇点(起点终点)外,其他点的度数均为偶数

有向图存在欧拉路径:起点出度=入度+1,终点入度=出度+1,其他路径上的点的出度=入度

无向图存在欧拉回路:当且仅当所有顶点度数为偶数,且为连通图

有向图存在欧拉回路:所有顶点的出度=入度且为连通图

算法

Fluery算法(没学)

Hierholzer算法

此算法可以找欧拉回路,而在找不到欧拉回路时会找欧拉路径

针对无向图的算法流程

void Hierholzer(int x)
{
    for(x所连到的点y)
    {
        删除边(x,y)
        删除边(y,x)
        Hierholzer(y);
    }
    x压入答案栈
}

将栈顶元素依次弹出输出即可得到欧拉回路/路径

我还没碰到有向图求欧拉回路的题目,暂时猜想针对有向图只需删去”删除边(y,x)”


思考1:

为什么不能写成

void Hierholzer(int x)
{
    输出x
    for(x所连到的点y)
    {
        删除边(x,y)
        删除边(y,x)
        Hierholzer(y);
    }
}

此算法又称圈套圈算法,如果所给图为两个相连的环的话,可能会先跑完其中一个环,直接输出了其中的一个环上的路径


思考2:

在只要求输出路径,而不要求路径顺序(指无向图,有向图一定不能这样)的时候把“将x压入答案栈”改成“输出x”也可以吗?

我以为是可以的,但是在写完下面的例题1后我发现我似乎不能解决这个问题,但暂时未举出不可以的反例


tip:为了更方便地删边,当点数不多时,我们可以用邻接矩阵存图,删边时直接\(G[x][y]\)–

由于涉及欧拉回路的题目较少,讲解较少,推荐读者结合例题理解欧拉回路

比较

听说后者编程复杂度和时间复杂度都优于前者,但是前者应用广泛性好

例题

T1

P2731 骑马修栅栏 Riding the Fences

分析

此题要求求出无向图的欧拉路径/回路

当然这类题一般都会叫你输出字典序最小的答案

针对这道题考虑几个细节:

1.顶点从1~500编号,但它并没有保证输入给的顶点编号连续(如告诉你1-2,2-5,5-6),

所以我用mn存下给出点的最小值,用mx存下给出点的最大值,用v[x]表示x点是否在输入给出

2.虽然输入数据保证至少又一个解,

但我的代码中还是写了一个并查集判断连通性(这也是存在欧拉回路的必要条件)

3.我针对是否存在欧拉回路分类,记下了每个点的度deg[x],如果有deg[x]&1=1即deg[x]为奇数则说明有奇点,我将从最小的奇点往下搜,

如果没有deg[x]&1=1则说明有欧拉回路,我从所给最小点往下搜

(当然我这里偷了个懒,题目说输入保证有解所以我直接考虑欧拉路径是不是欧拉回路,如果不保证输入有解,即还需判断是否有欧拉路径,则需在并查集判断连通性的基础上再看是否仅存在两个奇点)

4.要输出字典序最小解,则需要注意在dfs时从可连到的最小点往下搜

代码

Talk is cheap,show you the code.

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
using namespace std;
const int N=505;
const int M=1050;
const int inf=0x7fffffff;
int m;
int mn=inf,mx=-inf;
int deg[N];
int G[N][N];
stack <int> s;

int f[N];bool v[N];
int getf(int x){return f[x]==x?x:f[x]=getf(f[x]);}
void merge(int x,int y)
{
	int fx=getf(x);
	int fy=getf(y);
	if(fx!=fy) f[fx]=fy;
}

void Hierholzer(int x)
{
	for(int i=mn;i<=mx;i++)
		if(G[x][i])
		{
			G[x][i]--;
			G[i][x]--;
			Hierholzer(i);
		}
	s.push(x);
}
int main()
{
	for(int i=1;i<=500;i++) f[i]=i;
	scanf("%d",&m);
	for(int i=1;i<=m;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		G[x][y]++;
		G[y][x]++;
		deg[x]++;deg[y]++;
		v[x]=1;v[y]=1;
		merge(x,y);
		mn=min(mn,x);
		mn=min(mn,y);
		mx=max(mx,x);
		mx=max(mx,y);
	}
	for(int i=mn;i<=mx;i++)
		for(int j=mn;j<=mx;j++)
			if(v[i]&&v[j]&&getf(i)!=getf(j))
			{
				printf("No Solution!\n");
				return 0;
			}
	for(int i=mn;i<=mx;i++)
		if(v[i]&&(deg[i]&1))
		{
			Hierholzer(i);//找欧拉路径 
			break;
		}
		else if(i==mx) Hierholzer(mn);//从最小点找欧拉回路 
	while(!s.empty())
	{
		printf("%d\n",s.top());
		s.pop();
	}
	return 0;
}

其他想法:(只是我个人学习过程中的思考,只想简单知道这个算法怎么写的可以跳过)

上面的思考2中,我认为在只要求输出路径,而不要求路径顺序(指无向图,有向图一定不能这样)的时候把“将x压入答案栈”可以改成“输出x”

因为我思考了这样一个例子(参考博客所给)(按本题输入格式)

8
1 2
2 3
3 4
4 5
5 6
6 3
3 7
7 1

那么我不用栈,在不改变起点(此例中为1)的情况下,将

void Hierholzer(int x)
{
	for(int i=mn;i<=mx;i++)
		if(G[x][i])
		{
			G[x][i]--;
			G[i][x]--;
			Hierholzer(i);
		}
	s.push(x);
}

改成

void Hierholzer(int x)
{
	for(int i=mx;i>=mn;i--)
		if(G[x][i])
		{
			G[x][i]--;
			G[i][x]--;
			Hierholzer(i);
		}
	printf("%d\n",x);
}

输出了同一正确答案:1 2 3 4 5 6 3 7 1

但是,这种改法连洛谷所给样例都过不了(交上去也仅能A一个点)

为什么?

我们来分析一下,先从这个过不去的样例入手

9
1 2
2 3
3 4
4 2
4 5
2 5
5 6
5 7
4 6

其实不用,这个就行

2
1 2
2 3

改之后会输出:3 2 1

显然一条链的情况它就会出问题

那么我继续想,对于不是回路的欧拉路径,它只要搜到了路径终点,就会直接输出而出错,那么如果是欧拉回路的话这样可以吗?

抱歉,由于我太菜了,这个问题暂时不能解决,目前找不出能hack的欧拉回路数据,这种改法能过洛谷的第一个测试点,而经测试,剩余九个点都是欧拉路径而非回路,所以希望想思考这个问题的读者能帮我这个蒟蒻解决它

T2

P1341 无序字母对

分析

这题把“无序字母对”当做边,字母当作点

就是求无向图欧拉路径

相对上题,只是多了一个判断是否存在欧拉路径而已

注意怎么把不同字母映射到不同数字上就可以

代码

Talk is cheap,show you the code.

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
using namespace std;
const int N=26*2+5;
const int M=26*26+5;
const int inf=0x7fffffff;
int m;
int mx=-inf,mn=inf;
int G[N][N];
stack <int> sta;
int deg[N];

int f[N];
int getf(int x){return f[x]==x?x:f[x]=getf(f[x]);}
void merge(int x,int y)
{
	int fx=getf(x);
	int fy=getf(y);
	if(fx!=fy) f[fx]=fy;
}

int F(char c)
{
	if('A'<=c&&c<='Z') return int(c-'A'+1); 
	if('a'<=c&&c<='z') return int(c-'a'+27);
}
char FF(int x)
{
	if(1<=x&&x<=26) return char('A'+x-1);
	if(27<=x&&x<=52) return char('a'+x-27); 
}
void Hierholzer(int x)
{
	for(int i=mn;i<=mx;i++)
		if(G[x][i])
		{
			G[x][i]--;
			G[i][x]--;
			Hierholzer(i);
		}
	sta.push(x);
}
char s[5];
int main()
{
	for(int i=1;i<=26*2;i++) f[i]=i;
	scanf("%d",&m);
	for(int i=1;i<=m;i++)
	{
		scanf("%s",s);
		int x=F(s[0]),y=F(s[1]);
		merge(x,y);
		G[x][y]++;G[y][x]++;
		deg[x]++;deg[y]++;
		mx=max(mx,x);
		mx=max(mx,y);
		mn=min(mn,x);
		mn=min(mn,y);
	}
	for(int i=mn;i<=mx;i++)
		for(int j=mn;j<=mx;j++)
			if(deg[i]&&deg[j]&&getf(i)!=getf(j))//别忘了加条件deg[i]&&deg[j]
			{
				printf("No Solution\n");
				return 0;
			}
	int cnt=0;//奇点个数 
	for(int i=mn;i<=mx;i++) if(deg[i]&1) cnt++;
	if(cnt==0||cnt==2)
	{
		for(int i=mn;i<=mx;i++)
			if(deg[i]&1)
			{
				Hierholzer(i);
				break;
			}
			else if(i==mx) Hierholzer(mn);
		while(!sta.empty())
		{
			printf("%c",FF(sta.top()));
			sta.pop();
		}
	}
	else printf("No Solution\n");
	return 0;
}

本人太菜QAQ,如果有错误请指出哦QAQ


2019.3.30