目录

    讲课:欧拉路

    讲课:欧拉路

    定义

    图论概念采用 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\) 连通且图中每个顶点的度数都是偶数,即

    \[(\forall v \in V(G))\ ((\deg v) \bmod 2 = 0). \]

    一张有向图 \(G = (V, E)\) 是欧拉图,当且仅当 \(G\) 连通且图中每个顶点的入度等于出度,即

    \[(\forall v \in V(G))\ (\deg^- v = \deg^+ v). \]

    关键就是要让每个点进去几次就出来几次。

    一张无向图 \(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 算法就是利用该性质,将图分解为若干个回路。再串联起来。

    1. 选择一个起点。对于欧拉图,起点可以任选;对于半欧拉图,选择上面提到的能作为起点的顶点。
    2. 从起点出发,任选一条边走过去并将其删除,直到无路可走。
    3. 此时我们必然已经到达终点。对于欧拉图,终点就是起点;对于半欧拉图,终点就是上面提到的能作为终点且不是起点的顶点。有几种情况:
      1. 运气特别好,刚好形成一条欧拉路。
      2. 更大的概率是,在走的过程中,我们跳过了一些环。他们因为还没被走,所以还没被删除,留在图中,但他们一定是通过某些顶点依附在我们已经走了的那条迹上。
    4. 依次遍历每个已经走过的顶点:
      1. 如果他还有其他边,就从该顶点出发,递归地执行一次 Hierholzer 算法。
      2. 算法必然返回一条欧拉回路。将这条欧拉回路插入我们之前得到的路径中,该顶点所在的位置。

    模板题 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|)\)

    posted @ 2026-08-16 02:03  我就是蓬蒿人  阅读(33)  评论(0)    收藏  举报