图论真是一个神秘的东西,本来觉得欧拉图不就是一个图,一种图嘛。
结果没想到,在研究过程中,发现了越来越多的问题。全部延展开来,使这个小问题,变成了一堆巨大的问题。
另外,图论与数学推导真的密切相关,如果想要理解到本质是真的很累+很难。
我尽量讲的简化一点,明白一点。
如果有错误,望大佬们指出。
1、欧拉图的定义与起源
2、欧拉图的算法推演过程(性质与定理)
3、几种算法思路
4、参考算法模板?
5、各种测评题
欧拉图是个经典的图论分析代表,很具有研究图论思想的价值。但是我不知道到底哪种情况下该使用欧拉图。
欧拉图是指通过图(无向图或有向图)中所有边且每边仅通过一次通路,相应的回路称为欧拉回路。具有欧拉回路的图称为欧拉图(Euler Graph),具有欧拉通路而无欧拉回路的图称为半欧拉图。
其实分解下来定义精华:
欧拉回路——图G中经过每条边一次并且仅一次的回路称作欧拉回路。
(一笔画)
其实就是这个图里包含的所有边,我全都走了,并且都只有一次,就可以说这是一个欧拉路(不一定回到出发点)。我最后回到的是出发点就是欧拉回路。
(这个说法虽然挺不严密,但是很便于理解。)
这里给一个图解,大家理解一下:
半欧拉图——存在欧拉路但不存在欧拉回路的图称为半欧拉图。
欧拉图——存在欧拉回路的图称为欧拉图。
欧拉图听名字就知道发明创作者是谁,七桥问题其实是欧拉提出的论文中图论的第一篇论文“哥尼斯堡七桥问题”。
在当时的哥尼斯堡城有一条横贯全市的普雷格尔河,河中的两个岛与两岸用七座桥连结起来。当时那里的居民热衷于一个难题:有游人怎样不重复地走遍七桥,最后回到出发点。(其实故事挺迷的,大家都好闲)
为了解决这个问题,欧拉用 A,B,C,D 4个字母代替陆地,作为 4 个顶点,将联结两块陆地的桥用相应的线段表示,于是哥尼斯堡七桥问题就变成了图中,是否存在经过每条边一次且仅一次,经过所有的顶点的回路问题了。欧拉在论文中指出,这样的回路是不存在的。
然后巨佬就给出了专业的解析过程。
欧拉把每一块陆地考虑成一个点,连接两块陆地的桥以线表示。
后来推论出此种走法是不可能的。
他的论点是这样的,除了起点以外,每一次当一个人由一座桥进入一块陆地(或点)时,他(或她)同时也由另一座桥离开此点。所以每行经一点时,计算两座桥(或线),从起点离开的线与最后回到始点的线亦计算两座桥,因此每一个陆地与其他陆地连接的桥数必为偶数。
手推此处
在使用欧拉图的时候:
我们经常需要判定一个图是否为欧拉图(或半欧拉图),并且找出一条欧拉回路(或欧 拉路径)。
在此因主题原因,故不再讨论半欧拉图,只考虑欧拉图。
之前欧拉对于七桥问题的解决思路很好的给我们判断欧拉图是否成立,一些思路。
还是有严谨证明思路与定理来支撑我们判断欧拉图。
欧拉图的定理与性质(常用部分):
**这一部分我是从巨佬的博客下引用过来的,精简了一下
在此声明非本人原创,传送门:大佬“ DZYO” 的博客
另外,证明我是真的没有完全看懂。
一、
无向图G为欧拉图,当且仅当G为连通图且所有顶点的度为偶数。
证明:
(系统证明需要从必要性与充分性两个面来进行证明,即充分必要证明)
1、必要性。
设图G的一条欧拉回路为C。由于C经过图G的每一条边,而图G没 有孤立点,所以C也经过图G的每一个顶点,G为连通图成立。而对于图G的任意一个顶点 v,经过C时都是从一条边进入,从另一条边离开,因此v经过C的关联边的次数为偶数。又由于C不重复地经过了图G的每一条边,因此 的度为偶数。
2、充分性
(握手定理:有n个人握手,每人握手x次,握手总次数为S= nx/2。后推导出大量图论定理与性质。不是重点,但传送门:握手定理)
二、
定理2
有向图G为欧拉图,当且仅当G的基图连通,且所有顶点的入度等于出度。
tips:
1、证明类似于无向图。
2、另外,出度、入度类似于角在于高中阶段的定义,按照方向定义“出边”与“入边”。
所谓顶点的度(degree),就是指和该顶点相关联的边数。
入度 (in-degree) :以某顶点为弧头,终止于该顶点的弧的数目称为该顶点的入度
出度 (out-degree) :以某顶点为弧尾,起始于该顶点的弧的数目称为该顶点的出度
三、
性质1
设C是欧拉图G中的一个简单回路,将C中的边从图G中删去得到一个新的图G′′,则 G′′的每一个极大连通子图都有一条欧拉回路。
证明
若G为无向图,则图G′′的各顶点的度为偶数;若G为有向图,则图 G′′的各顶点的入度等于出度。
要想一个图G是欧拉图,图G需要满足两个条件:
针对有向图来说:
1.图G是连通的,不能有孤立的点存在。
2.每个顶点的入度要等于出度。
(离开此点与到达此点的边数相同。)
针对无向图来说:
1.图G是连通的,不能有孤立的点存在。
2.度数为奇数的点的个数为0。
无向图这里给了一个图解,大家可以看看。挺可爱的。有助理解,但它讲的方面不是很全,如果没有看明白也不用强迫着去看,忽略就好。
要推出几种算法思路,我们需要明确欧拉图的成立条件(上文已简单叙述)
如果只是仅仅需要运用欧拉回路,可直接运用下列算法。
欧拉图的算法都有一些缺陷,也有一些优点,请论情况使用。
由此可以推出欧拉图的算法有几种:
dfs算法思想
利用欧拉定理判断出一个图存在欧拉通路或回路后。
选择一个正确的起始顶点,用DFS 算法遍历所有的边(每条边只遍历一次),遇到走不通就回退。在搜索前进方向上将遍历过的边按顺序记录下来。这组边的排列就组成了一条欧拉通路或回路。
这个很简单但挺实用,大家应该对于dfs都很熟悉了,毕竟已经学了图论,不多赘述。
重点!!!
说句实话,fleury算法就是加上割边判定的深搜。
fleury虽然复杂度较差,但实现简单。(所以有什么用)
这个算法是较为高效地运用了图论思想,也是求解欧拉回路最有名且使用广泛的算法之一。
这个算法是弗勒里(B.H.Fleury) 在1883 年给出了在欧拉图中找出一个欧拉环游的多项式时间算法。
运用前我们需要明确“桥”(割边)的含义。
假设有连通图G,e是其中一条边,如果G-e是不连通的,则边e是图G的一条割边。
然后看算法。
其算法核心就是沿着一条迹往下寻找,先选择非割边,除非这个点的邻边都是割边。这样得到一条新的迹,然后再继续往下寻找,直到把所有边找完。遵循这样一个原则就可以找出图的一个欧拉回路来。
我下面虽然有实现模板,但我把重点截出来一下。
核心代码挺简单的,主要是对于割边的判断很重要。
并查集一般用来确定图是否连通,然后根据度来判断是否存在欧拉图,递归用来输出路路径。
但递归打印路径时候需要注意:必须先确定里面有欧拉路径,欧拉回路直接打印即可,欧拉通路需要找到起点。
有部分题需要用到这一种算法,但是实现真的挺复杂的。
另外,这玩意儿容易爆栈。
毕竟递归次数有点多。
所以防止爆栈的话用非递归。
这个方法挺迷,参考大佬的博客:dalao传送门
这一部分仅供参考,最好自己实现算法,毕竟欧拉图深搜挺简单的。
选了可读性稍微好一点的模板,主要大部分博客上的代码写得真的很乱,可读性差。前两个模板来自楼上提到的大佬。
1、查并集判断连通图
#include
#include
#define N 1000
using namespace std;
int n, m;
int f[N],degree[N];//记录第i点的度数
void init()
{
for (int i = 1; i <= n; i++)
f[i] = i;
}
int find(int x)
{
return x == f[x] ? x : f[x] = find(f[x]);
}
void merge(int x, int y)
{
int t1, t2;
t1 = find(x); t2 = find(y);
if (t1 != t2) f[t2] = t1;
else return;
}
int isEuler()
{
int cnt=0;
for (int i = 1; i <= n; i++)
{
if (degree[i] & 1)
cnt++;
if(cnt>2) return 0;
}
if(cnt==2||cnt==0) return 1;///cnt==2 欧拉通路 cnt==0欧拉回路
return 0;
}
int isconnect()
{
int cnt = 0;
int ff=find_(1);
for (int i=2; i <= n; i++)
{
if (find_(i)!=ff)
return 0;
}
return 1;
}
int main()
{
while (scanf("%d", &n) != EOF && n)
{
init();
memset(degree, 0, sizeof(degree));
scanf("%d", &m);
int t1, t2;
for (int i = 0; i < m; i++)
{
scanf("%d%d", &t1, &t2);
//输入有t1,t2相等的情况
if (t1 == t2)
continue;
degree[t1]++; degree[t2]++;
merge(t1, t2);
}
printf("%d\n", isEuler() && isconnect());
}
return 0;
}
2、输出路径
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ll long long
#define PI 3.1415926535897932384626
#define inf 0x3f3f3f3f
using namespace std;
const int maxn=6000;
struct Edge
{
int to,next,id;
bool flag;
}edge[maxn];
int head[maxn];
int in[maxn];
int tot;
void addedge(int u,int v,int w)
{
edge[tot].to=v;
edge[tot].next=head[u];
edge[tot].id=w;
edge[tot].flag=false;
head[u]=tot++;
}
int start;
vector<int>ans;
void init()
{
tot=0;
memset(head,-1,sizeof(head));
memset(in,0,sizeof(in));
}
void dfs(int x)
{
for(int i=head[x];i!=-1;i=edge[i].next)
{
if(!edge[i].flag)
{
edge[i].flag=true;
edge[i^1].flag=true;
dfs(edge[i].to);
ans.push_back(i);//记录边的号码
}
}
}
int main()
{
int x,y,c;
while(scanf("%d%d",&x,&y))
{
if(x==0&&y==0)break;
init();
scanf("%d",&c);
addedge(x,y,c);
addedge(y,x,c);
in[x]++;
in[y]++;
start=min(x,y);
while(scanf("%d%d",&x,&y))
{
if(x==0&&y==0)break;
scanf("%d",&c);
addedge(x,y,c);
addedge(y,x,c);
in[x]++;
in[y]++;
}
int flag=1;
for(int i=1;i<=maxn;i++) //判断是否为欧拉回路
{
if(in[i]%2!=0){
flag=0;
break;
}
}
if(flag==0)
{
puts("Round trip does not exist.");
continue;
}
ans.clear();
dfs(start);
for(int i=0;i<ans.size();i++)
{
if(i==0)
printf("%d",edge[ans[i]].id);
else
printf(" %d",edge[ans[i]].id);
}
cout<<endl;
}
return 0;
}
3、fleury算法实现
//欧拉路径的输出(此图为无向图)
#include
using namespace std;
#define M 200
struct stack
{
int top,node[M];
}s; //顶点的栈结构
int Edge[M][M]; //邻接矩阵
int n; //顶点个数
void dfs(int x) //这里的深度优先跟标准版有所区别,即不需要回溯
{
s.top++;
s.node[s.top]=x; //将即将要扩展的结点压入栈中
for(int i=0;i<n;i++)
{
if(Edge[i][x]) //如果该节点还存在边
{
Edge[i][x]=0;
Edge[x][i]=0; //删除该边,然后搜索另一结点
dfs(i);
break;
}
}
}
void Fleury(int x) //对头节点使用Fleury算法 查找欧拉路径
{
s.top=0;
s.node[s.top]=x;
while(s.top>=0)
{
int flag=0; //记录当前结点是否有边可以扩展
for(int i=0;i<n;i++)
{
if(Edge[i][s.top])
{
flag=1;
break;
}
}
if(!flag)
{
cout<<s.node[s.top]+1<<" "; //记录时是从0--n-1,所以应该加1
s.top--; //结点输出了,此结点出栈
}
else
{
s.top--; //因为dfs处理时,需要先进栈,所以这里要先出栈,然后再进栈
dfs(s.node[s.top+1]); //处理那个结点
}
}
cout<<endl;
}
int main()
{
int m,s,t; //边数,读入的边的起点和终点
int degree,num,start; //每个顶点的度、基度顶点个数、欧拉回路的起点
cin>>n>>m; //n---顶点数 m---边数
memset(Edge,0,sizeof(Edge));
for(int i=0;i<n;i++)
{
cin>>s>>t;
Edge[s-1][t-1]=1;
Edge[t-1][s-1]=1;
}
num=0;
start=0; //如果存在奇度顶点,则从奇度顶点出发,否则从顶点0出发
for(int i=0;i<n;i++)
{
degree=0;
for(int j=0;j<n;j++)
degree+=Edge[i][j];
if(degree%2)
{
num++;
start=i;
}
}
if(num==0||num==2) Fleury(start);
else cout<<"No Euler path"<<endl;
return 0;
}
难度排序:
模板题:
poj:1041 john’s trip
(无向图欧拉回路&路径输出,推荐必须要做的模板题,很经典。)
hdu:3018 ant trip
题解
(并查集:欧拉回路的问题)
一般难度:
POJ:1386 Play on Words
(判定有向图图欧拉路径是否存在)
POJ 1300 Door Man
(无向图欧拉通路和回路的判定+并查集)
究极(难一点的)难度:
大视野oj:太鼓达人
这道题是无意中翻到的,完全没看出来是欧拉图。需要强大的数学功底才能进行转化(要用欧拉回路判定+dfs+回溯做)。
这里选了一个题解(思路讲的较为清晰,有图解)
太鼓达人题解
另外,其实是为了表明我是音游狂热蒟蒻的身份才把这道题加上的。
uva:10735
是道紫题,此处给的是洛谷传送门,自己想要在uva上测评的给了题号,(我没用VPN上uva太卡了,便没给链接)。
洛谷题解挺详细,大家可以看看。
poj:1637 Sightseeing tour
一道好题(大佬如是说)。
要用混合图欧拉图(上面的uva也是这种),但这道题还要用网络流。
另外顺手收集的题目,想要刷题时可以做做。
另外题解:
dzyo大佬传送门
最后,
很多方面都参考了这位大佬,大家可以看一看他的blog。(前面也有传送门)
感谢各位大佬的博客!!!
欧拉图学习强调的是图论思想而非算法实现,要不然它的实现为什么都这么水
大家注意理解图论的本质就好,实现很简单的。
另外如果有问题欢迎大佬们指出!!!
哈密顿路径问题与哈密顿回圈问题属于数学中的图论。此问题是用来决定一个哈密顿图上的路径或回圈。两个问题皆为NP完全。为旅行推销员问题的特殊案例。
主要是因为这个问题现在还没有解决,所以资料很少。所以暂时不做系统研究,感兴趣可以自己gg。
我发现这是一个很神奇的东西,神奇到Wiki都没有提到过,欧拉图竟然是纯粹的图论情怀产物,一般遇不到,解决方法也很暴力,但主要重点在思维上。
我因为查阅资料的方式只有看别人的博客和Wiki,所以知识量有限,无法做到完全学术性精确。因此用这篇来简单讲一下。
其实对于欧拉图,我感觉它还是挺小众的,我经常做题看不懂题解什么的。而且大家都不怎么讲的清楚。反正做完感觉除了我做课题技术见长,代码实现能力还是很菜。
反正快乐过头。