欧拉回路
欧拉图
定义
- 欧拉回路:通过图中每条边恰好一次的回路
- 欧拉通路:通过图中每条边恰好一次的通路
- 欧拉图:具有欧拉回路的图
- 半欧拉图:具有欧拉通路但不具有欧拉回路的图
性质
欧拉图中所有顶点的度数都是偶数。
若
判别方法
无向图
-
无向图
是欧拉图,当且仅当:-
非零度顶点是连通的
-
顶点的度数都是偶数
-
-
无向图
是半欧拉图,当且仅当:-
非零度顶点是连通的
-
恰有 0 或 2 个奇度顶点
-
有向图
-
有向图
是欧拉图,当且仅当:-
非零度顶点是强连通的
-
每个顶点的入度和出度相等
-
-
有向图
是半欧拉图,当且仅当:-
非零度顶点是弱连通的
-
至多一个顶点的出度与入度之差为 1
-
至多一个顶点的入度与出度之差为 1
-
其他顶点的入度和出度相等
-
示例
以如下有向图为例
从顶点 1 出发,因为顶点 4 和顶点 5 的入度不等于出度,所以,它不存在欧拉通路。
如下有向图都存在欧拉通路:
求欧拉回路的方法
Fleury 算法
也称避桥法,是一个偏暴力的算法。
算法流程为每次选择下一条边的时候优先选择不是桥的边。
Hierholzer 算法
也称逐步插入回路法。
算法流程为从一条回路开始,每次任取一条目前回路中的点,将其替换为一条简单回路,以此寻找到一条欧拉回路。如果从路开始的话,就可以寻找到一条欧拉路。
实现
Hierholzer 算法的暴力实现如下:
性质
这个算法的时间复杂度约为
如果需要输出字典序最小的欧拉路或欧拉回路的话,因为需要将边排序,时间复杂度是
如果不需要排序,时间复杂度是
应用
计算机译码
设有
个字母,希望构造一个有 个扇形的圆盘,每个圆盘上放一个字母,使得圆盘上每连续 位对应长为 的符号串。转动一周( 次)后得到由 个字母产生的长度为 的 个各不相同的符号串。
构造如下有向欧拉图:
设
规定
顶点
边
这样的
任求
应用
应用1:洛谷 P2731 骑马修栅栏
题目
给定一张有
个顶点的无向图,求这张图的一条欧拉路或欧拉回路。如果有多组解,输出最小的那一组。
在本题中,欧拉路或欧拉回路不需要经过所有顶点。
边的数量满足 。
解题思路
用 Fleury 算法解决本题的时候只需要再贪心就好,不过由于复杂度不对,还是换 Hierholzer 算法吧。
保存答案可以使用 stack
注意,不能使用邻接矩阵存图,否则时间复杂度会退化为
代码实现
#include <algorithm> #include <cstdio> #include <stack> #include <vector> using namespace std; struct edge { int to; bool exists; int revref; bool operator<(const edge& b) const { return to < b.to; } }; vector<edge> beg[505]; int cnt[505]; const int dn = 500; stack<int> ans; void Hierholzer(int x) { // 关键函数 for (int& i = cnt[x]; i < (int)beg[x].size();) { if (beg[x][i].exists) { edge e = beg[x][i]; beg[x][i].exists = 0; beg[e.to][e.revref].exists = 0; ++i; Hierholzer(e.to); } else { ++i; } } ans.push(x); } int deg[505]; int reftop[505]; int main() { for (int i = 1; i <= dn; ++i) { beg[i].reserve(1050); // vector 用 reserve 避免动态分配空间,加快速度 } int m; scanf("%d", &m); for (int i = 1; i <= m; ++i) { int a, b; scanf("%d%d", &a, &b); beg[a].push_back((edge){b, 1, 0}); beg[b].push_back((edge){a, 1, 0}); ++deg[a]; ++deg[b]; } for (int i = 1; i <= dn; ++i) { if (!beg[i].empty()) { sort(beg[i].begin(), beg[i].end()); // 为了要按字典序贪心,必须排序 } } for (int i = 1; i <= dn; ++i) { for (int j = 0; j < (int)beg[i].size(); ++j) { beg[i][j].revref = reftop[beg[i][j].to]++; } } int bv = 0; for (int i = 1; i <= dn; ++i) { if (!deg[bv] && deg[i]) { bv = i; } else if (!(deg[bv] & 1) && (deg[i] & 1)) { bv = i; } } Hierholzer(bv); while (!ans.empty()) { printf("%d\n", ans.top()); ans.pop(); } }
应用2:Leetcode 753. 破解保险箱
题目
有一个需要密码才能打开的保险箱。密码是
位数, 密码的每一位都是范围 中的一个数字。
保险箱有一种特殊的密码校验方法,你可以随意输入密码序列,保险箱会自动记住 最后位输入 ,如果匹配,则能够打开保险箱。
在只知道密码位数和范围边界 的前提下,请你找出并返回确保在输入的 某个时刻 能够打开保险箱的任一 最短 密码序列。
用例:
输入:n = 2, k = 2
输出:"01100"
注意,"01100"、"10011" 和 "11001" 也可以确保打开保险箱。
解题思路
题意转换
求出一个最短的字符串,使其包含从
思路
将所有的
如果某个节点对应的数字为
我们以用例
如果我们从任一节点出发,能够找出一条路径,经过图中的所有边且只经过一次,然后把边上的数字写入字符串(还需加入起始节点的数字),那么这个字符串显然符合要求,而且找不出比它更短的字符串了。
Hierholzer 算法
由于这个有向图的每个节点都有
因此,我们可以用 Hierholzer 算法找出这条欧拉回路,这里,我们假设起始节点对应的数为
那么,最终的字符串为:
算法的思路如下:
-
从节点
开始,任意地经过还未经过的边,直到我们「无路可走」。此时我们一定回到了节点
,这是因为所有节点的入度和出度都相等。回到节点
之后,我们得到了一条从 开始到 结束的回路,这条回路上仍然有些节点 有未经过的出边。 -
我们从节点
开始,继续得到一条从 开始到 结束的回路,再嵌入之前的回路中,即以此类推,直到没有节点有未经过的出边,此时我们就找到了一条欧拉回路。
算法步骤:
-
使用深度优先的方式遍历有向图,从起始节点 "
" 出发,遍历它邻近的 个节点;假设当前节点为
,节点 的出边连接的节点为 ,那么: -
使用
记录访问过的边,已经访问过的节点不再重复访问;注意,下一个节点只需要取后
位即可。 -
在路径上添加最后的结束点
,结束节点就是起始节点。
这里,我们使用一个集合
代码实现
class Solution: def crackSafe(self, n: int, k: int) -> str: visited = set() path = list() highest = 10 ** (n - 1) # 从节点 "00...0"开始遍历 self.dfs(k, 0, visited, path, highest) # 添加结束节点,结束节点就是起点 path.append("0" * (n - 1)) return "".join(path) def dfs(self, k, node: int, visited, path, highest): # 遍历当前节点的邻居节点 for i in range(k): neighboor = node * 10 + i if neighboor not in visited: visited.add(neighboor) # 下一个节点最多取后 n-1 位即可 self.dfs(k, neighboor % highest, visited, path, highest) # 后序遍历记录路径 path.append(str(i)) return
class Solution { TreeSet<String> visited; StringBuilder path; public String crackSafe(int n, int k) { if(n == 1 && k == 1) { return "0"; } visited = new TreeSet<>(); path = new StringBuilder(); // 从顶点 00..0 开始 String start = String.join("", Collections.nCopies(n - 1, "0"));; findEuler(start, k); path.append(start); // 回路添加最后的 end 顶点,end 就是 start return path.toString(); // return new String(path); } public void findEuler(String curv, int k) { for(int i = 0; i < k; i ++) { // 往顶点的 k 条出边检查,顶点加一条出边就是一种密码可能 String nextv = curv + i; if(!visited.contains(nextv)) { visited.add(nextv); findEuler(nextv.substring(1), k); path.append(i); } } } }
应用3:Leetcode 332. 重新安排行程
题目
分析
题意转换
给定一个 n 个点 m 条边的图,要求从指定的顶点出发,经过所有的边恰好一次(可以理解为给定起点的「一笔画」问题),使得路径的字典序最小。
解题思路
因为本题保证至少存在一种合理的路径,也就告诉了我们,这张图是一个欧拉图或者半欧拉图。我们只需要输出这条欧拉通路的路径即可。
Hierholzer 算法用于在连通图中寻找欧拉路径,其流程如下:
-
从起点出发,进行深度优先搜索。
-
每次沿着某条边从某个顶点移动到另外一个顶点的时候,都需要删除这条边。
-
如果没有可移动的路径,则将所在节点加入到栈中,并返回。
代码实现
class Solution: def findItinerary(self, tickets: List[List[str]]) -> List[str]: vec = collections.defaultdict(list) for depart, arrive in tickets: vec[depart].append(arrive) for key in vec: heapq.heapify(vec[key]) stack = list() self.dfs("JFK", vec, stack) return stack[::-1] def dfs(self, curr: str, vec, stack): while vec[curr]: tmp = heapq.heappop(vec[curr]) self.dfs(tmp, vec, stack) stack.append(curr)
参考:
本文作者:LARRY1024
本文链接:https://www.cnblogs.com/larry1024/p/17443789.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步