树的世界很规矩:两点之间只有一条简单路。基环树只多了一条边,就突然多出一个能绕圈的地方。诀窍不是假装它还是树,而是先把那一圈找出来。
学完后,你能:
前置:树、度数、队列、DFS/BFS。
准备图 A-B,B-C,C-A,B-D,D-E,C-F,C-G。每个点旁先写当前度数;每删一个点,划掉它、给未删邻点的度数减 1,并写一句证词:“它为什么不可能在唯一环上?”不要一开始把剩余点叫“环”,先让删除过程证明它。
函数图实验另用一张纸:每点恰一条出箭头,记录的是入度。这两种剥离动作看起来相像,但删的理由不同。
画一棵 5 点树,例如 A-B,A-C,C-D,C-E。从 A 到 D 找路径;再尝试找一条不重复边的第二条路径。请数边数,并尝试删掉任意一条边。
停顿 0:为什么连通且无环的 5 点图必须有 4 条边?若有第 5 条边,它的两个端点原本在树中已有怎样的关系?
回到预设图。请填初始度数,圈出所有度数为 1 的点;然后假装你要在一个环上绕路,问:能否经过度数 1 的 E 后离开它而不重复边?
| 点 | A | B | C | D | E | F | G |
|---|---|---|---|---|---|---|---|
| 初始度数 | |||||||
| 被删后剩余度数 |
停顿 1:删 E 后 D 变成什么?为什么 D 这时才获得“绝不在环上”的证书?
请把无向剥叶的状态补全:
degree[u] = ______
queue = ______
removed[u] = ______
弹出 u 后,对每个 ______ 的邻点 v:______
停顿 2:为什么不直接把所有当前度数小于等于 1 的点反复扫描?队列替你省掉了什么重复劳动?
画箭头 A→B,B→C,C→A,D→C,E→D,F→E。沿每点唯一出边前进,先从 F 开始写路线。请标出所有入度为 0 的点并按队列删除。
停顿 3:为什么出度 1 的 F 仍可以先删?为什么删 F 后该减少 E 的入度而不是出度?
请把预设无向图分为“环 A-B-C-A”和挂在 B/C 上的树。若每个删掉的叶子有一个值要汇总到离环更近的点,应该在“入队、弹出、还是最后遍历环”哪个时刻累加?
若从任一点 DFS,把访问过的邻点全当父亲或已处理点,环会让“父子关系唯一”的假设失效;做树形 DP 时,环的首尾也会互相依赖。可如果先把所有挂在环外的树枝削掉,最后留下的正是唯一环,问题就分成“环外树”和“环上小问题”两部分。
一棵连通 $n$ 点树有 $n-1$ 边;额外加一条边会得到至少一个环。若存在两个不同简单环,删其中一个环上一条不被另一个环必需的边仍可能保持连通,边数便可继续降到低于树的下界,矛盾。更直观地说:从树加的那条边只有一次机会闭合原来的唯一树路径。
叶子不在环上,删掉它不会删掉任何环。删叶子后,它唯一邻点的度数减 1;这个邻点可能变成新的叶子,继续入队。直到队列空,所有剩余点度数都至少为 2;在连通单环图中,它们恰好组成那条环。
queue = every vertex whose degree is 1
removed[all] = false
while queue not empty:
u = queue.pop_front()
removed[u] = true
for v in neighbors[u]:
if not removed[v]:
degree[v] -= 1
if degree[v] == 1:
queue.push_back(v)
cycle_vertices = every vertex not removed
这和拓扑排序很像:不是按有向先修关系剥入度 0,而是按“绝不可能在环上”的无向叶子剥度数 1。
若每个点恰有一条出边,沿箭头一直走必会重复,因此每个弱连通块有一个有向环,其他点都是指向环的树。把入度为 0 的点不断删掉,剩下的就是环点;方向换了,但“剥掉不能在环上的部分”的思想没换。
queue = all vertices with indegree 0
while queue not empty:
u = queue.pop_front()
v = to[u]
indegree[v] -= 1
if indegree[v] == 0:
queue.push_back(v)
remaining vertices are directed-cycle vertices
对预设无向图,填写每一轮队列和度数变化:
| 弹出点 | 它的未删邻点 | 邻点新度数 | 新入队点 | 当前剩余点 |
|---|---|---|---|---|
| E | ||||
| F | ||||
| G | ||||
| D |
再对函数图写入度序列,按 F,E,D 删除。最后请分别写一句:“无向图剩余点为何组成环?”“函数图剩余点为何组成有向环?”
剥叶顺序天然从外向内。若每个被删点有一个要向父亲汇总的值,可以在弹出时累加到唯一未删邻点;最后每个环点拿到了自己挂树的完整信息。环上若有“相邻不能同时选”之类约束,常把一个点强制选或不选,做两次链 DP,避免把首尾当作互不相干。
整个剥叶过程每条边导致至多一次度数下降,复杂度 $O(n)$。适用前提很重要:无向部分须是连通 $n$ 点 $n$ 边;一般图可能剩多个环或复杂核心,不能把“剩余点都是唯一环”照搬。
degree == 1,函数图是 indegree == 0;别把两种剪刀拿反。n 点 n 边只保证全图环数总量,不保证每块各一个。运行 树多长出一条边.py。选择 u 可观察无向图从叶子到环的剥离顺序;选择 f 可观察函数图的入度剥离。程序会在每一步打印哪一个点已不可能在环上。
先预测预设图里 $D$ 何时变成叶子;然后改看函数图,比较“度数 1”和“入度 0”这两个相似却不同的条件。
基环树的“核心 + 挂树”分解会通向环上计数、删环边直径和函数迭代;更一般的图中,反复剥叶留下的称为 2-core。树形 DP 能处理挂树,环上的首尾依赖则提醒你不要把一圈强行掰成一条没有说明的链。