讲课:欧拉路
讲课:欧拉路
定义
图论概念采用 OI-wiki 的定义。
对于一张图,如果一条迹(trail)经过图的每条边恰好一次,则称他是一条欧拉路(Eulerian path);如果一条回路(circuit)经过图的每条边恰好一次,则称他是一条欧拉回路(Eulerian circuit)。按照这个定义,一条欧拉回路一定是一条欧拉路,但反之不一定成立。
请注意名称的误导性:欧拉路径并不一定是路径,而且大多数情况下不是。
如果一张图存在欧拉回路,则称他是一张欧拉图(Eulerian graph)。如果一张图不存在欧拉回路,但存在欧拉路,则称他是一张半欧拉图(semi-Eulerian graph)。
通俗地来说,欧拉图就是能一笔画并回到起点的图,半欧拉图就是能一笔画但不回到起点的图。
注意与哈密顿路径(Hamiltonian path)、哈密顿回路(Hamiltonian circuit)等概念相区分,他们关注的是经过图的每个顶点恰好一次。
哈密顿图、半哈密顿图的判定是 NP-完全的,求出一条哈密顿路径则是 FNP-完全的,最短哈密顿路径的判定是 NP-完全的,求出最短哈密顿路径(即旅行商问题(traveling salesman problem, TSP))则是 NP-困难的。除非 \(\mathrm{P} = \mathrm{NP}\),否则这些问题都没有多项式时间的算法,在竞赛中一般通过状压 DP 来解决。
判定
欧拉图和半欧拉图的判定是很容易的。
一张无向图 \(G = (V, E)\) 是欧拉图,当且仅当 \(G\) 连通且图中每个顶点的度数都是偶数,即
一张有向图 \(G = (V, E)\) 是欧拉图,当且仅当 \(G\) 连通且图中每个顶点的入度等于出度,即
关键就是要让每个点进去几次就出来几次。
一张无向图 \(G = (V, E)\) 是半欧拉图,当且仅当 \(G\) 连通且图中有恰好两个顶点 \(s,t \in V\ (s \neq t)\) 满足 \((\deg s) \bmod 2 = (\deg t) \bmod 2 = 1\),且 \(G\) 的任何欧拉路都必定以 \(s,t\) 为两端点。
一张有向图 \(G = (V, E)\) 是半欧拉图,当且仅当
- \(G\) 连通。
- 存在顶点 \(s \in V\) 满足 \(\deg^- s + 1 = \deg^+ s\)。
- 存在顶点 \(t \in V\) 满足 \(\deg^- t = \deg^+ t + 1\)。
- 其他每个顶点的入度都等于出度,即 \((\forall v \in V \setminus \{s, t\})\ (\deg^- v = \deg^+ v)\)。
且 \(G\) 的任何欧拉路都必定从 \(s\) 走到 \(t\)。
构造
欧拉图有一个非常关键的性质:可以被划分为若干条不共边的回路。下面介绍的 Hierholzer 算法就是利用该性质,将图分解为若干个回路。再串联起来。
- 选择一个起点。对于欧拉图,起点可以任选;对于半欧拉图,选择上面提到的能作为起点的顶点。
- 从起点出发,任选一条边走过去并将其删除,直到无路可走。
- 此时我们必然已经到达终点。对于欧拉图,终点就是起点;对于半欧拉图,终点就是上面提到的能作为终点且不是起点的顶点。有几种情况:
- 运气特别好,刚好形成一条欧拉路。
- 更大的概率是,在走的过程中,我们跳过了一些环。他们因为还没被走,所以还没被删除,留在图中,但他们一定是通过某些顶点依附在我们已经走了的那条迹上。
- 依次遍历每个已经走过的顶点:
- 如果他还有其他边,就从该顶点出发,递归地执行一次 Hierholzer 算法。
- 算法必然返回一条欧拉回路。将这条欧拉回路插入我们之前得到的路径中,该顶点所在的位置。
模板题 Luogu P7771 参考代码:
template <typename Weight, bool is_directed>
std::pair<size_t, std::vector<size_t>>
Graph<Weight, is_directed>::hierholzer(size_t src) const {
std::array<size_t, 2> ends{size_t(-1), size_t(-1)};
if (is_directed) {
std::vector<size_t> in_degs(m_adj.size()), out_degs(m_adj.size());
for (const auto &edge : m_edges) {
++out_degs[edge.u];
++in_degs[edge.v];
}
for (size_t i = 0; i < m_adj.size(); ++i) {
if (in_degs[i] == out_degs[i]) continue;
if (in_degs[i] + 1 == out_degs[i]) {
if (~ends[0]) {
return std::make_pair(size_t(-1), std::vector<size_t>());
}
ends[0] = i;
} else if (in_degs[i] == out_degs[i] + 1) {
if (~ends[1]) {
return std::make_pair(size_t(-1), std::vector<size_t>());
}
ends[1] = i;
} else {
return std::make_pair(size_t(-1), std::vector<size_t>());
}
}
if (~src && ~ends[0] && src != ends[0]) {
return std::make_pair(size_t(-1), std::vector<size_t>());
}
} else {
std::vector<size_t> degs(m_adj.size());
for (const auto &edge : m_edges) {
++degs[edge.u];
++degs[edge.v];
}
for (size_t i = 0; i < m_adj.size(); ++i) {
if (!(degs[i] & 1)) continue;
if (~ends[0]) {
if (~ends[1]) {
return std::make_pair(size_t(-1), std::vector<size_t>());
}
ends[1] = i;
} else {
ends[0] = i;
}
}
if (~src && ~ends[0] && src != ends[0] && src != ends[1]) {
return std::make_pair(size_t(-1), std::vector<size_t>());
}
}
if (static_cast<bool>(~ends[0]) != static_cast<bool>(~ends[1])) {
return std::make_pair(size_t(-1), std::vector<size_t>());
}
if (src == size_t(-1)) {
src = (~ends[0] ? ends[0] : 0);
}
assert(~src && src < m_adj.size());
std::vector<size_t> idxs(m_adj.size());
std::vector<bool> used(m_edges.size());
std::vector<size_t> res;
res.reserve(m_edges.size());
std::function<void(size_t)> dfs = [&](size_t cur) -> void {
for (auto &i = idxs[cur]; i < m_adj[cur].size(); ++i) {
auto e = m_adj[cur][i];
if (used[e]) continue;
auto &edge = m_edges[e];
if (is_directed && edge.u != cur) continue;
auto nxt = ((edge.u == cur) ? edge.v : edge.u);
used[e] = true;
dfs(nxt);
res.push_back(e);
}
};
dfs(src);
if (res.size() != m_edges.size()) { // not connected
return std::make_pair(size_t(-1), std::vector<size_t>());
}
std::reverse(res.begin(), res.end());
return std::make_pair(src, res);
}
时间复杂度:\(\Theta(|V| + |E|)\)。
空间复杂度:\(\Theta(|V| + |E|)\)。

浙公网安备 33010602011771号