什么是欧拉回路
从一个点出发,沿着边行走,经过每一个边恰好一次(要经过所有的边),之后在回到出发点。 有哈密尔顿回路,并不一定存在欧拉回路。
欧拉回路一定遍历了所有顶点。但是存在欧拉回路是不是一定存在哈密尔顿回路呢? 其实不是的,因为哈密尔顿回路要求每一个顶点只能遍历一次。欧拉回路不要求每一个顶点只遍历一次。
欧拉回路是欧拉研究格尼斯堡七桥问题发现的。 也就是通常大家说的 一笔画问题 很长时间没有人能解决这个问题。他很长时间也没有解决这个问题。最后欧拉尝试证明这个问题没有解。这是图论领域的开端。
欧拉回路存在的性质
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-exmEZ4Eq-1587987613919)(/Users/xieyunfei/Library/Application Support/typora-user-images/image-20200427094320801.png)]
对于每一个点,我们进去一次,出去一次,就要耗费两条边。为什么是耗费两条边呢? 因为这两条边之后不能再走了。
对于欧拉回路来说,每个边都走一遍,回到原点。 每个点都必须有进有出。每个点相连的边数必须是偶数。 (每一点的度都必须是偶数)
上面这个图就没有欧拉回路。对于无向连通图来说,每个点的度是偶数<---->图存在欧拉回路 。对于每个点的度数都是偶数—> 无向连通图是存在欧拉回路,我们是怎么知道的呢? 现在我们来证明一下[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Un7na8mG-1587987613922)(/Users/xieyunfei/Library/Application Support/typora-user-images/image-20200427095157344.png)]
这个图所有的顶点度数都是偶数,这个图一定有环。我们从任意一个顶点出发,先随便找一个环。如果这个环就是原图,已经找了欧拉回路。否则,剩下的边一定和我们找到了环相连,并且所有的顶点的度数依然是偶数,也就是依然存在环。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v4RfqEfg-1587987613923)(/Users/xieyunfei/Library/Application Support/typora-user-images/image-20200427100144402.png)]
剩下的环和之前的环是有公共点的。 两个相连的环一定可以组成新的环。
判断一个图是否存在欧拉回路
在整个图是连通的情况下,我们直接验证每个顶点的度 是不是偶数。如果每个顶点的度是偶数,那么,这个图一定是存在欧拉回路的。
import java.util.LinkedList;
import java.util.Queue;
public class EulerLoop {
private Graph g;
private boolean[] visited;
public EulerLoop(Graph g) {
this.g = g;
visited = new boolean[g.V()];
}
// 判断整个图是否联通
private boolean CC() {
bfs(0);
return AllVisited();
}
//判断是不是所有的顶点都已经被访问过
private boolean AllVisited() {
for (int i = 0; i < visited.length; i++) {
if (!visited[i])
return false;
}
return true;
}
// BFS进行以s为起点的图的连通分量的遍历
private void bfs(int s) {
visited[s] = true;
Queue<Integer> queue = new LinkedList<>();
queue.offer(s);
while (!queue.isEmpty()) {
int v = queue.poll();
for (int w : g.adj(v)) {
if (!visited[w]) {
visited[w] = true;
queue.offer(w);
}
}
}
}
//首先要进行连通性判断
public boolean hasEulerLoop() {
if (!CC()) // 如果不连通 直接返回false
return false;
for (int v = 0; v < g.V(); v++) {
if (g.degree(v) % 2 != 0)
return false;
}
return true;
}
}
如何寻找欧拉回路
一般有三种方法,我们这里实现最简单的一中,Hierholzer算法,这种算法实现起来比较简单,也很容易理解。
回溯法(指数级别)
与寻找哈密尔顿回路相比,我们把遍历顶点变成遍历边。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UW0I686s-1587987613927)(/Users/xieyunfei/Library/Application Support/typora-user-images/image-20200427111242063.png)]
Fleury算法(贪心法)
如果有多条边可以选择·的话,不走桥。 我们在寻找桥的过程是不能预处理的。 我们在寻找欧拉回路的过程中在不断删除边。因此寻找桥需要进行动态寻找。
Hierholzer 算法(线性算法)
(首先我们要确定这个图是连通的)时间复杂度O(E ) 需要使用一个栈,一个叫做 curPath 当前路径,从某个起始点出发,随便找一个环,这个环的路径放在curPath 中。每次,当我们访问到一个顶点,如果这个顶点的度为0 , 说明我们找到了一个环。然后我们从栈中把顶点依次退栈,把度为0的顶点加入结果列表中,直到退栈的顶点度不为0,(这个顶点就是两个环的公共点,我们需要用这个顶点 把不同的环串起来)再次进行遍历。
import java.util.ArrayList;
import java.util.Stack;
public class Hierholzer {
private Graph G;
private boolean[] visited;
public Hierholzer(Graph G) {
visited = new boolean[G.V()];
this.G = G ;
}
public boolean hasEulerLoop() {
dfs(0);
return Allvisited();
}
private boolean Allvisited() {
for (int i = 0; i < G.V(); i++) {
if (!visited[i])
return false;
}
return true;
}
private void dfs(int s) {
visited[s] = true;
for (int w : G.adj(s)) {
if (!visited[w])
dfs(w);
}
}
// 核心算法
public ArrayList<Integer> result() {
ArrayList<Integer> res = new ArrayList<>();
if (!hasEulerLoop()) return res;
Graph g = (Graph) G.clone();
Stack<Integer> stack = new Stack<>();
int curv = 0;
stack.push(curv);//第一次放入的0 完全是为了满足while 循环条件的
while (!stack.isEmpty()) {
if (g.degree(curv) != 0) {//当顶点curv 的度不为0时
stack.push(curv); // 把curv放入栈中
int w = g.adj(curv).iterator().next(); // 找出curve下一个没有访问的顶点
g.removeEdge(curv, w); // 然后删除curve 和 w 之间的顶点
curv = w; // 这里我觉得用 过河拆桥 比较形象
} else { // 当一个点的度为0时,说明此时恰好找到了一个环
res.add(curv);
curv = stack.pop(); // 回退-->我们找回退路上第一个度不为0的,也就是环与环之间的枢纽
}
}
return res;
}
public static void main(String[] args){
Graph g = new Graph("g5.txt") ;
Hierholzer hierholzer = new Hierholzer(g);
System.out.println(hierholzer.result());
}
}