欧拉回路
欧拉环:图中经过每条边一次且仅一次的环;
欧拉路径:图中经过每条边一次且仅一次的路径;
欧拉图:有至少一个欧拉环的图;
半欧拉图:没有欧拉环,但有至少一条欧拉路径的图。
【无向图】
一个无向图是欧拉图当且仅当该图是连通的(注意,不考虑图中度为0的点,因为它们的存在对于图中是否存在欧拉环、欧拉路径没有影响)且所有点的度数都是偶数;一个无向图是半欧拉图当且仅当该图是连通的且有且只有2个点的度数是奇数(此时这两个点只能作为欧拉路径的起点和终点);
证明:因为任意一个点,欧拉环(或欧拉路径)从它这里进去多少次就要出来多少次,故(进去的次数+出来的次数)为偶数,又因为(进去的次数+出来的次数)=该点的度数(根据定义),所以该点的度数为偶数。
【有向图】
一个有向图是欧拉图当且仅当该图的基图(将所有有向边变为无向边后形成的无向图,这里同样不考虑度数为0的点)是连通的且所有点的入度等于出度;一个有向图是半欧拉图当且仅当该图的基图是连通的且有且只有一个点的入度比出度少1(作为欧拉路径的起点),有且只有一个点的入度比出度多1(作为终点),其余点的入度等于出度。
证明:与无向图证明类似,一个点进去多少次就要出来多少次。
【无向图、有向图中欧拉环的求法】
与二分图匹配算法类似,是一个深度优先遍历的过程,时间复杂度O(M)(因为一条边最多被访问一次)。核心代码(边是用边表存储的而不是邻接链表,因为无向图中需要对其逆向的边进行处理,在有向图中,可以用邻接链表存储边):
[cpp]view plaincopyprint?
1. void dfs(int x)
2. {
3. int y;
4. for (int p=hd[x]; p != -1; p=ed[p].next) if (!ed[p].vst) {
5. y = ed[p].b;
6. ed[p].vst = 1;
7. ed[p ^ 1].vst = 1; //如果是有向图则不要这句
8. dfs(y);
9. res[v--] = y + 1;
10. }
11. }
如何寻找欧拉回路、欧拉通路(套圈法)
算法思想的朴素表达
对于欧拉图,从一个节点出发,随便往下走(走过之后需要标记一下,下次就不要来了),必然也在这个节点终止(因为除了起始节点,其他节点的度数都是偶数,只要能进去就能出来)。这样就构成了一个圈,但因为是随便走的,所以可能会有些边还没走过就回来了。我们就从终止节点逆着往前查找,直到找到第一个分叉路口,然后从这个节点出发继续上面的步骤,肯定也是可以找到一条回到这个点的路径的,这时我们把两个圈连在一起。当你把所有的圈都找出来后,整个欧拉回路的寻找就完成了。
寻找欧拉回路时,起始节点是可以任意选择的。如果是有基度顶点要寻找欧拉通路,则从基度顶点出发就好了,上述步骤依然有效。
算法思想的书面表达
一个解决此类问题基本的想法是从某个节点开始,然后查出一个从这个点出发回到这个点的环路径。现在,环已经建立,这种方法保证每个点都被遍历.如果有某个点的边没有被遍历就让这个点为起点,这条边为起始边,把它和当前的环衔接上。这样直至所有的边都被遍历。这样,整个图就被连接到一起了。
更正式的说,要找出欧拉路径,就要循环地找出出发点。按以下步骤:
任取一个起点,开始下面的步骤
如果该点没有相连的点,就将该点加进路径中然后返回。
如果该点有相连的点,就列一张相连点的表然后遍历它们直到该点没有相连的点。(遍历一个点,删除一个点)
处理当前的点,删除和这个点相连的边,在它相邻的点上重复上面的步骤,把当前这个点加入路径中.
[cpp] view plaincopy
1. //主函数中调用下面这个函数
2. euler(start, -1); //因为直接从start出发,所以第二个参数用-1代替,输出的时候要忽略掉。
3. //euler函数就是用的传说中的套圈法,是一个迭代的过程
4. //npath是一个全局变量,记录已经找到的边的数量
5. //边存储在adj[]中,是用数组实现的链表结构(就跟很多hash那样,首节点的数据域不用,只有指针部分有效)
6. void euler(int cur, int edgeN) //cur当前到达的节点 edgeN上一被选择的边,即上一个节点通过edgeN到达的cur
7. {
8. int i;
9. while(adj[cur].nxt != -1)
10. {
11. i=adj[cur].nxt;
12. adj[cur].nxt=adj[i].nxt;//相当于是删除掉使用了的边
13. euler(adj[i].end,i);
14. }
15. path[npath++] = edgeN; //后序记录,如果要保持搜索时候边的优先级,则逆向输出
16. }
要注意的是在res中写入是逆序的,所以初始的v应设成(边数-1)。
但是有一个问题是,这是递归实现的,当点数过多时有爆栈危险,所以最好使用非递归:
[cpp]view plaincopyprint?
1. void dfs()
2. {
3. int x = 0, y, tp = 1; stk[0] = 0;
4. re(i, n) now[i] = hd[i];
5. bool ff;
6. while (tp) {
7. ff = 0;
8. for (int p=now[x]; p != -1; p=ed[p].next) if (!ed[p].vst) {
9. y = ed[p].b;
10. ed[p].vst = 1;
11. ed[p ^ 1].vst = 1; //如果是有向图则不要这句
12. now[x] = p; stk[tp++] = y; x = y; ff = 1; break;
13. }
14. if (!ff) {
15. res[v--] = x + 1;
16. x = stk[--tp - 1];
17. }
18. }
19. }
Fleury(弗罗莱)算法求欧拉路径
算法在运行过程中删除了所有已走的路径,也就是说途中残留了所有没有行走的边。根据割边的定义,如果在搜索过程中遇到割边意味着当前的搜索路径需要改进,即提前输出某一个联通子集的访问序列,这样就能够保证访问完其中联通子图中后再通过割边访问后一个联通子图,最后再沿原路输出一开始到达该点的路径。如果只有割边可以扩展的话,只需要考虑先输出割边的另一部分联
#include
#include
#include
#include
#include
usingnamespace std;
/*
弗罗莱算法
*/
int stk[1005];
int top;
int N, M, ss, tt;
int mp[1005][1005];
void dfs(int x) {
stk[top++] = x;
for (int i = 1; i <= N; ++i) {
if (mp[x][i]) {
mp[x][i] = mp[i][x] = 0;//删除此边
dfs(i);
break;
}
}
}
/*
9 12
1 5
1 9
5 3
5 4
5 8
2 3
2 4
4 6
6 7
6 8
7 8
8 9
path:
4 5 8 7 6 8 9 1 5 3 2 4 6
*/
void fleury(int ss) {
int brige;
top = 0;
stk[top++] = ss; //将起点放入Euler路径中
while (top >0) {
brige = 1;
for (int i = 1; i <= N; ++i) {//试图搜索一条边不是割边(桥)
if (mp[stk[top-1]][i]) {
brige = 0;
break;
}
}
if (brige) {//如果没有点可以扩展,输出并出栈
printf("%d ", stk[--top]);
} else {//否则继续搜索欧拉路径
dfs(stk[--top]);
}
}
}
int main() {
int x, y, deg, num;
while (scanf("%d %d", &N, &M) != EOF) {
memset(mp, 0,sizeof (mp));
for (int i = 0; i < M; ++i) {
scanf("%d %d", &x, &y);
mp[x][y] = mp[y][x] = 1;
}
for (int i = 1; i <= N; ++i) {
deg = num = 0;
for (int j = 1; j <= N; ++j) {
deg += mp[i][j];
}
if (deg %2 ==1) {
ss = i, ++num;
printf("%d\n", i);
}
}
if (num ==0 || num ==2) {
fleury(ss);
} else {
puts("No Euler path");
}
}
return0;
}