线性版本HierHolzer正确性说明

晚上在研究怎么求欧拉图回路,看到 O(n+m) 版本的 HierHolzer 算法实现,让我很迷惑。

void dfs(int x){
	for(int i = 1;i <= 500; ++i){
		if(g[x][i]){
			--g[x][i]; --g[i][x];
			dfs(i);
		}
	}
	ans[++cnt] = x;
}

OI-Wiki 上对于这段代码的描述是这样的:

将找回路的 DFS 和 Hierholzer 算法的递归合并,边找回路边使用 Hierholzer 算法。

但代码中我并没有直观地看到“边找回路边求环”的过程,而只看到了从一个点出发,有路就往下走的朴素 dfs。所以我就产生了这样的一个问题:这样的 dfs 是否能正确地找到欧拉回路呢?


首先根据欧拉图判别法我们可以知道:欧拉图中非零度顶点是连通的,且顶点的度数都是偶数。从这个性质出发,我们在脑海中随便画出一张欧拉图,以检验算法的准确性。

接下来开始证明(由于我不会做动画,所以只能尽我所能直观地描述了):

我们将这张图上度数为偶数的节点染为白色,将度数为奇数的节点染为黑色,每经过一条边就将其删去并更新颜色。显然,最开始欧拉图上都是白色的节点。

任意选取一个点开始我们的朴素 dfs。当我们走过第一条边的时候,这条边直接连接的两个顶点(起点和当前位置)都变成了黑色。当我们继续向下 dfs 的时候,每走过一条边,原先位置的黑色节点会变回白色,而当前所在的节点会变为黑色。显然,在向下 dfs 的过程中,图上要么没有黑点,要么有且仅有两个黑点。

当我们访问的某条边连接了两个黑点时,删去这条边,整张图上所有的节点又会都变为白色,此时我们就找到了(删去了)一个经过起点的环。

到这一步 dfs 并没有结束,我们也没有考虑清楚如何记录答案,但 HierHolzer 已经初具雏形了。如果称上述过程(从起点向下 dfs 又到了起点)为一次操作,我们可以发现这次操作具有如下的性质:

在一张全是白点的图上,从任意一个度数非 0 的节点(度数当然是偶数)出发,必然能找到(删去)经过这个点的一个环。(注意欧拉图上偶度节点与“必然”能找到环的联系)


继续 dfs,此时我们正第二次位于起点的位置。回顾一下我们所制定的 dfs 规则:如果有路,就一直往下走。

按照这个规则,如果此时起点的度数大于 0,那么就继续往下走。换句话说,也就是重复一次上述的操作。可想而知,每一个经过起点的环都能用上述的方法找到。每经过这样的一个过程,就会删去一个经过起点的环,起点的度数就会减 2,直到它变为 0

如果此时起点的度数为 0,那么按照 dfs 的规则,我们就可以开始回溯了。注意此时回溯的顺序,如果我们是按照 1231 的顺序访问,那么我们应该按照 1321 的顺序回溯。每回溯到一个节点,这个节点的度数都必然是偶数,那么我们就可以重复一次操作,删去一个经过它的环。直到最后回溯到起点,我们就删去了图上所有的环。

显然,在回溯时将当前点添加到答案路径中,我们就可以将这若干个环拼成一个完整的路径。而由于这条路径的起点(所有边已经都删光,从起点回溯的时候)和终点(回溯到起点)都是我们所指定的那个起点,所以这条路径是一条回路。

综上,我们就找到了一条符合条件的欧拉回路。


这个过程还可以进行推广。
比如半欧拉图上找欧拉通路的问题,就等价于过程中图上有且仅有两个黑点的情况。
比如每个环输出的顺序和遍历的顺序相反但起点不变,如果想要字典序最小欧拉回路,就要优先走编号小的节点,最后倒序输出。


至此我们再回看 HierHolzer 的流程,看似复杂而割裂的“找环——遍历环——找环”的过程就这样完美地融入在了一次 dfs 的过程中。

image

当然,如果有一天你需要脱离模板敲下完整的 HierHolzer,现场推一遍显然是不现实的。所以我们只需要坚定一个信念:

附模板题 P2731AC 代码

posted @   LittleDrinks  阅读(33)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示