图论中有这么一类问题,涉及到欧拉图、欧拉路径(一笔画问题)、欧拉回路。本文给出其(不严谨的)定义,一些结论,最后给出了Hierholzer 算法以及对应的例题和答案。
(不严谨的)定义对于一个连通的
图G,有
欧拉路径
的问题叫做一笔画问题
。
欧拉回路:一条路径,它能够不重复地遍历完所有的边,并且回到起点。可以看出欧拉回路
也是欧拉路径
。
半欧拉图:一个图,图中存在欧拉路径
。
欧拉图:一个图,图中存在欧拉回路
。可以看出欧拉图
也是半欧拉图
。
图与欧拉路径、欧拉回路的结论
连通的无向图
为(半)欧拉图的条件:
欧拉回路
。反之也成立,即若能够找到从任意顶点出发的欧拉回路
,则所有顶点的度为偶数。
若有且仅有2个顶点的度数为奇数,则只能够找到欧拉路径
(路径从这两点中的任一顶点出发,到另一顶点结束)。反之也成立。
连通的有向图
为(半)欧拉图的条件:
欧拉回路
。反之也成立。
若有且仅有两个顶点入度不等于出度,其中一个顶点入度比出度大1,记为V1V_1V1,另一个顶点入度比出度小1,记为V2V_2V2,则只能够找到欧拉路径
(路径从顶点V2V_2V2出发,到顶点V1V_1V1结束)。反之也成立。
连通的混合图
为(半)欧拉图的条件:(混合图
是指既有有向边又有无向边的图。)
这个有待考究
)
Hierholzer 算法
问题简述:给定一个(半)欧拉图,求欧拉路径。
Hierholzer 算法思想:当给定的图一定有欧拉路径(回路
)时,从一个合理的
起始点出发(后面会说什么是合理的),深度优先遍历整个图,遍历过的顶点都不得再遍历,直到遇到的第一个没有可遍历的邻居
的顶点,这个顶点一点是某条欧拉路径的终点,把这个顶点“删掉”(实际上不用删,通过标记边已访问就可以不再访问它)后,下一次遇到的没有可遍历的邻居
的顶点,一定是这条欧拉路径倒数第二个顶点,再把这个顶点“删掉”再遍历,以此类推,直到把所有没有可遍历的邻居
的顶点找到,我们就找到了这条欧拉路径上的所有顶点。:
问题1:可能有人会奇怪,Hierholzer 算法为什么一定能得到欧拉路径?为什么每遇到没有可遍历的邻居
的顶点就是欧拉路径上的一个终点?下面以有向图作为说明,无向图同理。
这个其实涉及到一个顶点的出度和入度问题。
若从某个顶点开始遍历,遍历过的边不能在遍历,直到无边可遍历为止,当遇到另一个出度等于入度顶点VVV时,是不可能停留在VVV的,因为VVV出度等于入度,你进入
多少次,一定有对应的出边让你出
去多少次。
因此第一个遇到的没有可遍历的邻居
的顶点只有两种,一种是它的入度比出度大一,另一种是它的入度与出度相等,但是它是起点(也就是说既是起点又是终点,饶了一圈)。根据前面所说的一些结论(图与欧拉路径、欧拉回路的结论),这两种点都是某条欧拉路径上的终点,所以当我们遇到的没有可遍历的邻居
的顶点,尽管放心大胆的把该顶点记录下来,因为它一定是欧拉路径的终点。
问题2:可能还有人会奇怪,为什么遇到的第二个没有可遍历的邻居
的顶点是欧拉路径倒数第二个点?我们可以想象一下,我把第一个没有可遍历的邻居
的顶点以及对应的边“删掉”后(实际也可以不删,只需标记已经访问过就行),它相邻顶点的出度和入度就会发生变化,在草稿纸画一下你就会发现,它周围的顶点要么变成出度等于入度的顶点,要么变成入度比出度大1的顶点,他们都符合成为欧拉路径的终点的条件,并且由于我们是递归地深度优先遍历,递归返回上一层后,一定是在这些相邻顶点之中,所以这时候遇到的第二个没有可遍历的邻居
的顶点,一定是欧拉路径倒数第二个点。
Hierholzer 算法过程:
选择一个合理的点
作为起始点,遍历所有相邻边。(一会说什么是合理的点
)
深度优先搜索,访问相邻顶点。将经过的边都不能再访问。
如果当前顶点没有相邻边,则将顶点入数组末尾。
最后将数组倒序输出,就是从起点出发的欧拉回路。
Hierholzer 算法作用:个人觉得,Hierholzer 算法
就是证明了,当给定的图一定有欧拉路径(回路
)时,按照Hierholzer 算法
无脑深度优先搜索,就一定会得到欧拉路径(回路)
的逆序。至于得到的是欧拉路径
还是欧拉环路
,取决于你的图是欧拉图
还是半欧拉图
,若图为欧拉图
,得到的是欧拉环路
,若图是半欧拉图
,得到的则是欧拉路径
。Hierholzer 算法为什么一定能得到欧拉路径?它的原理是什么?请看这里。
注意: 若图不是欧拉图
也不是半欧拉图
,采用Hierholzer 算法
得到的结果必定错误。所以在贸然采用Hierholzer 算法
前,我们需要先按照前面说的图与欧拉路径、欧拉回路的结论判断图到底是不是(半)欧拉图
,若是,才能用该算法去找欧拉路径(回路)
。
什么是合理的
起始点:上面说到选择合理的点
作为起始点,那么什么点是合理的
?这里需要回顾一下前面说的图与欧拉路径、欧拉回路的关系,以无向图为例(有向图同理):
欧拉回路
,此时从任一点出发,都能找到欧拉回路,因此任何一点都是合理的
;
当图为半欧拉图时,有且仅有2个顶点的度数为奇数,只能够找到欧拉路径
(路径从这两点中的任一点出发,到另一点结束),此时只有从这两点之一出发,才能找到欧拉路径,因此只有这两点是合理的
。
例题:为了掌握Hierholzer 算法,这里给出一道例题【leetcode】332. 重新安排行程
例题答案(C++):
// 思路:
// Hierholzer算法。个人觉得Hierholzer算法就是证明了一点:当存在欧拉路径时,从合理的起始点无脑dfs遍历,得到的路径一定是欧拉路径。
// 因为题目规定了一定有欧拉路径,并且起点一定是JFK(所以这个起始点一定是合理的),所以根据Hierholzer算法,可以无脑dfs。
class Solution {
public:
// 这里用map,内部自动按照string升序排列了,所以先找到的一定是自然排序最小的路径
typedef unordered_map<string, map> adjacent;
vector min_path;
bool dfs(adjacent &adj, string airport){
// 无脑dfs遍历邻居,同时遍历过的边标记已遍历
for(auto &[next, number] : adj[airport]){
if(0 >= number)
continue;
--number;
dfs(adj, next);
}
// 终点是没有相邻边的点
// 当删除终点后,终点前的点也没有相邻边了,变成新的终点
// 运行到这里,当前airport一定没有可遍历的相邻边了,则它是此时的终点
min_path.push_back(airport);
return true;
}
vector findItinerary(vector<vector>& tickets) {
// 初始化邻接表,因为存在多张相同机票的情况,所以邻接表中还记录了从from到to的机票数
adjacent adj;
for(auto & t : tickets){
if(adj.find(t[0]) == adj.end())
adj[t[0]] = map();
if(adj[t[0]].find(t[1]) == adj[t[0]].end())
adj[t[0]][t[1]] = 0;
adj[t[0]][t[1]]++;
}
// Hierholzer算法
dfs(adj, "JFK");
// Hierholzer算法得到结果为终点到起点的路径,需要反转才是题目所要求的结果
std::reverse(min_path.begin(), min_path.end());
return min_path;
}
};
相关/参考链接
『图论』入门以及 Hierholzer 算法
欧拉回路/路径【总结】
无向图求欧拉路径,回路 模板(Hierholzer 算法)