学完后,你应能:
前置:DFS、栈、图的度数、弱连通。
设任务是“从仓库出发,恰好走完每条道路一次”。普通 DFS 的目标是访问顶点,它看见已访问顶点就可能拒绝一条尚未使用的边;即使改成标记边,直接输出 DFS 到达顺序,也可能在某个点过早走进死路。
最小例子是两个环共用一个顶点。若先走完其中一个环,局部上已经回到公共点;另一环仍可插入这条闭合路线。这个“把后来找到的回路插入已有路线”的能力,正是 Hierholzer 算法的核心。
在无向路径内部,每次进入一个点,都要用另一条尚未使用的边离开,因此被使用的关联边两两配对。只有起点可以多一次“离开”,终点可以多一次“进入”。所以:
有向图把“配对”改成一次入边配一次出边。记
$$ \Delta(v)=\operatorname{out}(v)-\operatorname{in}(v). $$
回路要求所有 $\Delta(v)=0$;开放路径要求恰有一个点 $\Delta(v)=+1$(起点)、一个点 $\Delta(v)=-1$(终点),其余为 $0$。
度数只描述每个点附近的平衡,不能把两块互不相连的边粘起来。无向图要求所有非零度点属于同一连通块;有向图只需先检查忽略方向后的弱连通,再结合入出度条件。
停顿:为什么有向图不必额外手写一次“强连通检查”?
维护一个栈 st 和每个点的邻接表游标:
为什么“死端倒序入答案”不会破坏相邻性?弹出 $v$ 时,栈中前一个点与 $v$ 之间正是把 $v$ 压栈的那条边;后发现的子回路会在倒序中自然插入。
每条邻接记录只被游标扫过一次,因此时间为 $O(V+E)$,空间为 $O(V+E)$。
| 图类型 | 度数条件 | 连通条件 | 起点 |
|---|---|---|---|
| 无向欧拉回路 | 所有非零度点为偶数 | 非零度点连通 | 任一非零度点 |
| 无向欧拉路径 | 恰有两个奇度点 | 非零度点连通 | 任一奇度点 |
| 有向欧拉回路 | 所有点入度 = 出度 | 非零度点弱连通 | 任一非零度点 |
| 有向欧拉路径 | 恰有一个点 $\Delta=+1$、一个点 $\Delta=-1$,其余 $\Delta=0$ | 非零度点弱连通 | $\Delta=+1$ 的点 |
空边图要按题意单独处理:数学上可把单点长度零行走看作回路,但输出协议未必接受。
Hierholzer 的证明可以拆成三件事:
无向图的一条边在邻接表中出现两次,必须共享同一个 edge_id;标记边而不是标记方向。若题目把一条无向边明确要求来回各走一次,则应建成两条独立有向边,不能共享使用标记。
迭代写法避免深度达到 $E$ 时递归爆栈:
stack = [start]
reversed_path = []
while stack is not empty:
v = stack[-1]
advance cursor[v] past every already-used edge
if v has no unused edge:
reversed_path.append(stack.pop())
else:
edge = take edge at cursor[v]
mark edge used
stack.append(edge.to)
path = reverse(reversed_path)
最终务必检查路径使用边数:有 $E$ 条边时,顶点序列长度应为 $E+1$。它能抓住遗漏连通性检查、起点选错和建图错误。
有 $N$ 个牧场和 $M$ 条双向道路,要求从 1 出发并回到 1,每条道路恰走两次,且两个方向各一次。$2\le N\le 10^4$,$1\le M\le 5\times10^4$;允许重边,题目保证存在方案。输出经过的 $2M+1$ 个顶点。
样例中每条无向边在输出路线里各以两个方向出现一次;输出不唯一。
对每条输入 $u-v$ 加入弧 $u\to v$ 和 $v\to u$,从 1 运行 Hierholzer。
每条弧只被游标消费一次,所以原道路的两个方向各出现一次;算法输出相邻点均由被消费的弧连接。题目保证所有需用边能从 1 所在路线覆盖,故最终路径含 $2M+1$ 个点,开头和结尾均为 1。时间 $O(N+M)$,空间 $O(N+M)$。
for each road (u, v):
add_arc(u, v); add_arc(v, u)
stack = [1]; reversed_route = []
while stack:
u = stack[-1]
if u still has an unused outgoing arc: stack.append(take_one_arc(u))
else: reversed_route.append(stack.pop())
output reverse(reversed_route)
完整 GNU C++17 参考实现见附件:POJ-2230.cpp
识别信号是“每条无向边按每个方向各一次”。若改成“每条无向边总共一次”,就必须共享边编号并检查奇度点;若要求字典序最小路线,需要先按目标顺序组织邻接边,但仍要在最终答案反转这一点上小心顺序方向。
每个单词是一块牌,若前一个单词的末字母等于后一个单词的首字母,就能相接。判断能否把全部 $N$ 个单词各用一次排成一条链。每组 $1\le N\le10^5$,单词由小写字母组成,长度为 2 到 1000;单词可重复。
对单词 $s$ 加一条 $s0\to s{|s|-1}$ 的边,统计入出度,并在忽略方向的 26 点图上合并两个端点。若所有出现过的字母属于同一连通块,且入出度差全部为 0,或恰有一个 $1$ 与一个 $-1$,则有欧拉链;反之,任一合法单词排列本身就是欧拉链,必满足这些条件。
读取成本与字符总数线性;额外算法成本 $O(N+26\alpha(26))$,空间 $O(26)$。
for each word:
add directed edge first_letter -> last_letter
update in_degree, out_degree, and undirected DSU
connected = all nonzero-degree letters have one DSU root
degree_ok = all differences are 0, or exactly one +1 and one -1
answer = connected and degree_ok
完整 GNU C++17 参考实现见附件:HDU-1116.cpp
识别信号是“对象只由首尾决定相容性,且每个对象恰用一次”。若要求输出具体单词顺序,邻接边要保存单词编号并运行 Hierholzer;若要求字典序最小,还要明确比较的是单词本身还是最终拼接串。
未知密码长度为 $n+2$。现在给出它的全部 $n$ 个长度为 3 的连续子串,并保留重复次数;字符来自大小写英文字母与数字。判断是否存在相符的字符串,若存在输出任意一个。$1\le n\le2\times10^5$。
定义字符编码 0..61,把二元串编码成 a*62+b。每个输入三元串建立从前两字符到后两字符的边。按有向欧拉路径条件选起点,运行 Hierholzer;若结果不是 $n+1$ 个点则输出 NO。
若算法输出路径 $v_0,v_1,\ldots,v_n$,每条相邻转移对应且只对应一份输入三元串。拼接 $v_0$ 的两个字符,再依次追加 $v_i$ 的末字符,得到长度 $n+2$ 的串,其每个长度 3 窗口恰是路径中的对应边。反过来,任何合法密码的相邻窗口顺序都给出一条欧拉路径。因此算法充要。
时间 $O(n+62^2)$,空间 $O(n+62^2)$,适合 $2\times10^5$。
encode every length-2 string as a vertex
for trigram xyz: add edge xy -> yz
check directed Euler degree conditions and select start
path = Hierholzer(start)
if len(path) != edge_count + 1: print NO
else: print first 2-char vertex, then append each later vertex's last char
完整 GNU C++17 参考实现见附件:CodeForces-508D.cpp
识别信号是“给出全部固定长度窗口并恢复序列”。推广到长度 $k$ 时,点是长度 $k-1$ 的串。若 $k$ 较大,不能直接用 $|\Sigma|^{k-1}$ 数组,应给出现过的窗口哈希编号;若要求字典序最小答案,则要控制边的选择顺序并重新确认反转后的顺序。
给一张可不连通的简单无向图,把每条边定向,使入度等于出度的顶点尽量多,并输出最大数量及任一最优定向。每组 $1\le n\le200$,$0\le m\le n(n-1)/2$;所有测试的 $n$ 之和不超过 200。
统计原图偶度点数作为答案。为每个奇度点添加边 $0-v$,然后对增广图的所有含边连通块运行无向 Hierholzer,并按实际走边方向记录每条原边的方向。
上界由奇偶性给出。构造中每个原偶度点没有虚拟边,欧拉回路对其贡献成对的一入一出,删虚拟边后仍平衡;因此所有偶度点都达到上界,构造最优。每条增广边只处理一次,时间 $O(n+m)$,空间 $O(n+m)$。
原图可能有多个全偶度连通块,它们不与虚拟点 0 相连,所以增广图被分成多个含边连通块;必须从每个尚有未用边的点启动一次 Hierholzer。边方向在“取出边并压栈”时记录即可。
answer = number of even-degree original vertices
add dummy vertex 0 and edge 0-v for every odd-degree vertex v
for every augmented connected component:
follow an Euler circuit and orient each traversed original edge forward
discard dummy edges and output recorded original orientations
完整 GNU C++17 参考实现见附件:CodeForces-723E.cpp
本题最重要的不是“会欧拉”,而是先用奇偶性写出答案上界,再用欧拉回路让所有可行点同时达到上界。若目标改成让 |out-in|≤1 的点最多,奇度点也可能合格,上界与补边方式都会改变。
给出顶点编号相同的两棵 $N$ 点根树。构造整数 $X_1,ldots,X_N$,使两棵树中每个顶点的整棵子树之和的绝对值都为 $1$;无解时报告。$1\le N\le10^5$,两行父亲数组各恰描述一棵合法根树。
设第一、二棵树中 $v$ 的孩子数分别为 $c_v,d_v$。对子树和取模 2 得
$$ X_v+c_v\equiv1\pmod2, \qquad X_v+d_v\equiv1\pmod2. $$
所以 $c_v\not\equiv d_v\pmod2$ 时无解。
否则建立 $2N+1$ 点辅助图:第一层放第一棵树的全部无向树边,第二层放第二棵树的树边;两棵树的根分别连公共点 $R$。当 $c_v$ 为偶数时,再连跨层边 $P_v-Q_v$。对 $P_v$ 而言,树边加根边共贡献 $c_v+1$ 度:$c_v$ 偶时再加跨层边,$c_v$ 奇时不加,两种情况度数都为偶数;第二层同理。辅助图连通且全点偶度,故存在欧拉回路。
沿回路方向定向所有边。跨层边 $P_v\to Q_v$ 时令 $X_v=1$,反向令 $X_v=-1$;无跨层边令 $X_v=0$。在第一层对任一子树内顶点做“入度 = 出度”求和,内部树边抵消,只剩子树根通向父亲/公共点的一条边与跨层边净流,因此子树内 $X$ 之和为 $\pm1$;第二层完全相同。这同时证明合法性和完备性。建图与 Hierholzer 都是 $O(N)$ 时间、$O(N)$ 空间。
if parity(child_count_tree1[v]) differs from tree2 for some v:
report IMPOSSIBLE
build two tree layers P and Q plus common vertex R
connect both roots to R
for each v with even child count: add cross edge P[v] -- Q[v]
tour = Euler_circuit(auxiliary_graph)
for each v:
X[v] = 0 if there is no cross edge
otherwise X[v] = +1 or -1 according to its traversal direction
父亲数组含唯一的 -1 根;辅助图的无向边必须共享编号。实现采用迭代 Hierholzer,避免 $N=10^5$ 的递归深度。完整 GNU C++17 附件:AtCoder-agc018_f.cpp
识别信号不是“两棵树”,而是“每个层次集合的和只能为 $\pm1$”。若目标绝对值改成任意 $K>1$,模 2 只给必要条件,单条跨层边也不再足以表达边界净流,本构造失效。
给定简单连通无向图,问能否把全部边不重不漏地分成恰好三条 circuit;circuit 允许重复顶点但不允许重复边。$1\le N,M\le10^5$。
先检查所有度为偶数。若有点度至少 6,欧拉回路在该点产生至少三段闭合子行走,可合并多余段得到恰三条。以下最高度数为 4。若四度点至少三个,沿欧拉回路观察它们的重复出现,可以在三个分叉处重新配对边段得到三条 circuit。
若四度点少于两个,压缩全部二度链后的连通偶图至多只有一个分叉,不可能有三条非空 circuit。恰有两个四度点 $A,B$ 时,删除 $B$ 后检查 $A$ 是否仍位于某个环上:若是,该环作为第一条,余下偶度边含连接 $A,B$ 的两组边段,可分成另两条;若否,所有从 $A$ 出发的四条压缩边都通向 $B$,任一 circuit 必须消耗两条 $A-B$ 链,最多只能得到两条。一次 DFS 即可判定,时间 $O(N+M)$、空间 $O(N+M)$。
if some degree is odd: No
if some degree >= 6: Yes
four = vertices with degree 4
if len(four) >= 3: Yes
if len(four) <= 1: No
A, B = four
if DFS can leave A and return to A without visiting B: Yes
else: No
简单图条件让 DFS 中按父点跳过返边足够;若允许重边,必须改用父边编号,否则两条平行边组成的环会被漏掉。完整 GNU C++17 附件:AtCoder-agc032_c.cpp
这题的迁移点是“先压缩所有行为被迫的二度点,再分类少数分叉点”。若题目要求输出三条 circuit,判定结论仍可用,但还需保存压缩链并在欧拉序上落实切分点。
Fair Share 共用考虑链,放在这里做迁移训练。Tanya and Password 中为什么三元串必须是边而不是点?One-Way Reform 的最优性上界来自什么不变量?used,何时不共享?Play on Words,限时 25 分钟完成“点/边选择 + 充要判定”。Tanya and Password 卡 30 分钟可依次看 Hint 1–3;复盘重点是窗口重叠建图。One-Way Reform 即使不提交,也要自己写出“上界—构造达到上界”的两段证明,并构造一个含多个连通块的测试。