欧拉图
定义
- ** 欧拉回路**:通过图中每条边恰好一次的回路
- 欧拉通路:通过图中每条边恰好一次的通路
- 欧拉图:具有欧拉回路的图
- 半欧拉图:具有欧拉通路但不具有欧拉回路的图
性质
欧拉图中所有顶点的度数都是偶数。
若 \(G\) 是欧拉图,则它为若干个环的并,且每条边被包含在奇数个环内。
判别方法
无向图
-
无向图 \(G\) 是欧拉图,当且仅当:
-
非零度顶点是连通的
-
顶点的度数都是偶数
-
-
无向图 \(G\) 是半欧拉图,当且仅当:
-
非零度顶点是连通的
-
恰有 0 或 2 个奇度顶点
-
有向图
-
有向图 \(D\) 是欧拉图,当且仅当:
-
非零度顶点是强连通的
-
每个顶点的入度和出度相等
-
-
有向图 \(D\) 是半欧拉图,当且仅当:
-
非零度顶点是弱连通的
-
至多一个顶点的出度与入度之差为 1
-
至多一个顶点的入度与出度之差为 1
-
其他顶点的入度和出度相等
-
示例
以如下有向图为例
从顶点 1 出发,因为顶点 4 和顶点 5 的入度不等于出度,所以,它不存在欧拉通路。
如下有向图都存在欧拉通路:
求欧拉回路的方法
Fleury 算法
也称避桥法,是一个偏暴力的算法。
算法流程为每次选择下一条边的时候优先选择不是桥的边。
Hierholzer 算法
也称逐步插入回路法。
Hierholzer 算法的暴力实现如下:
性质
这个算法的时间复杂度约为 \(O(nm+m^2)\)。实际上还有复杂度更低的实现方法,就是将找回路的 \(DFS\) 和 \(Hierholzer\) 算法的递归合并,边找回路边使用 Hierholzer 算法。
如果需要输出字典序最小的欧拉路或欧拉回路的话,因为需要将边排序,时间复杂度是 \(\Theta(n+m\log m)\)(计数排序或者基数排序可以优化至 \(\Theta(n+m)\)。
如果不需要排序,时间复杂度是 \(\Theta(n+m)\)。
应用
计算机译码
设有 \(m\) 个字母,希望构造一个有 \(m^n\) 个扇形的圆盘,每个圆盘上放一个字母,使得圆盘上每连续 \(n\) 位对应长为 \(n\) 的符号串。转动一周(\(m^n\) 次)后得到由 \(m\) 个字母产生的长度为 \(n\) 的 \(m^n\) 个各不相同的符号串。
构造如下有向欧拉图:
设 \(S = \{a_1, a_2, \cdots, a_m\}\),构造 \(D=\langle V, E\rangle\),如下:
规定 \(D\) 中顶点与边的关联关系如下:
顶点 \(a_{i_1}a_{i_2}\cdots a_{i_{n-1}}\) 引出 \(m\) 条边:\(a_{i_1}a_{i_2}\cdots a_{i_{n-1}}a_r, r=1, 2, \cdots, m\)。
边 \(a_{j_1}a_{j_2}\cdots a_{j_{n-1}}\) 引入顶点 \(a_{j_2}a_{j_3}\cdots a_{j_{n}}\)。
这样的 \(D\) 是连通的,且每个顶点入度等于出度(均等于 \(m\)),所以 \(D\) 是有向欧拉图。
任求 \(D\) 中一条欧拉回路 \(C\),取 \(C\) 中各边的最后一个字母,按各边在 \(C\) 中的顺序排成圆形放在圆盘上即可。
应用
应用1:洛谷 P2731 骑马修栅栏
题目
给定一张有 \(500\) 个顶点的无向图,求这张图的一条欧拉路或欧拉回路。如果有多组解,输出最小的那一组。
在本题中,欧拉路或欧拉回路不需要经过所有顶点。
边的数量 \(m\) 满足 \(1\leq m \leq 1024\)。
解题思路
用 Fleury 算法解决本题的时候只需要再贪心就好,不过由于复杂度不对,还是换 Hierholzer 算法吧。
保存答案可以使用 stack
注意,不能使用邻接矩阵存图,否则时间复杂度会退化为 \Theta(nm)。由于需要将边排序,建议使用前向星或者 vector 存图。示例代码使用 vector。
代码实现
#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();
}
}
参考: