若“一个点为什么能属于多个块”还没有画清,先完成 课前零食:从共享房间到圆方树。它从暴力删点、边栈结账走到圆方图,附有可操作的 Python 验算。
学完后,你应能判断割点(articulation point),用边栈提取点双连通分量(vertex-biconnected component, v-BCC),区分点双与边双,并理解为什么点双分量重叠于割点、需要用圆方树表达。
前置:无向 DFS、上一节的桥与 dfn/low。
low[v] >= dfn[u] 与 low[v] > dfn[u] 分别对应什么?逐个删点再检查连通性需要 $O(V(V+E))$。在 DFS 树中,非根点 $u$ 是割点,当且仅当存在儿子 $v$ 使
$$ low[v]\ge dfn[u]. $$
此时 $v$ 子树不能绕过 $u$ 到达 $u$ 的祖先,删掉 $u$ 就把它隔开。根没有祖先,判据改成“DFS 树儿子至少两个”。
若还要取出点双分量,只标割点不够。每次走树边或指向祖先的返祖边时,把边编号压栈;当 low[v] >= dfn[u] 时,从栈顶弹到树边 $(u,v)$,这些边及其端点组成一个点双分量。
为什么用边栈而不是点栈?割点属于多个点双分量,不能在第一次弹出后永久删除;边却恰属于一个这样的块。
| 概念 | 删除对象 | 块之间怎样相交 | 典型缩点结构 |
|---|---|---|---|
| 强连通分量 | 有向可达 | 点不重叠 | DAG |
| 边双连通分量 | 删除一条边仍连通 | 点不重叠 | 桥树/森林 |
| 点双连通分量 | 删除一个点后仍保持块内连通 | 可共享割点 | 圆方树/块割树 |
圆方树中,原顶点作为圆点,每个点双分量新建方点,并连接方点与属于该分量的原顶点。原图连通时得到一棵树;路径问题由“必须经过哪些割点”转成树路径问题。
对树边 $u-v$,若 low[v] < dfn[u],子树能绕过 $u$ 回到更早祖先,不在此处封闭。若 low[v] >= dfn[u],从该树边之后压入的边无法与更早边共同处于一个更大的点双分量,应在这里恰好弹出。每条边压栈、弹栈至多一次,总时间 $O(V+E)$,空间 $O(V+E)$。
edge_id 跳父边;重边不能按父顶点全部跳过。dfn[v] < dfn[u] 的已访问边压栈,避免同一无向边压两次。每组给一张连通无向电话网络,$N<100$。某交换站失效后,若除它自身外还造成其他站之间不能通信,则该站是关键点。求关键点数量。输入的每行以一个点开头,后接与它直接相连的点;单独一行 0 结束本组,$N=0$ 结束输入。
从任一点 DFS。对非根 $u$,若某儿子子树无法连接到 $u$ 的祖先,删 $u$ 后该子树与其余图断开;反之每个儿子都能绕到祖先,删 $u$ 后仍可汇合。根只有多个独立儿子时才会造成分裂。标记后计数即可,复杂度 $O(N+M)$。
DFS(u, parent_edge):
dfn[u] = low[u] = next_time()
for each edge u-v except parent_edge:
if v unvisited:
DFS(v); low[u] = min(low[u], low[v])
if u is non-root and low[v] >= dfn[u]: mark u as cut
else: low[u] = min(low[u], dfn[v])
root is cut iff it has at least two DFS children
完整 GNU C++17 参考实现见附件:POJ-1144.cpp
若要输出“删除该点后增加多少个连通块”,非根应统计满足判据的儿子数并结合原图连通块;不能只用一个布尔标记。
给定可不连通的简单无向图,$1\le n\le10^5$,$0\le m\le\min(n(n-1)/2,10^5)$。求恰好属于一个简单环的边,按编号递增输出。
Tarjan DFS 时维护边栈,每遇到点双边界就弹出一块。用时间戳数组统计该块不同端点数;若边数等于点数,把块中全部边加入答案。
每条简单环完全包含在一个点双分量中。若块满足 $E_c=V_c$,点双连通迫使每点度数至少 2,而度数总和为 $2E_c=2V_c$,所以每点度数恰为 2,整块就是一个简单环。若 $E_c>V_c$,点双块含额外耳结构,使其中边不可能只属于唯一简单环。故筛选充要。复杂度 $O(n+m)$。
during undirected Tarjan, push tree/back edge IDs on a stack
when low[child] >= dfn[parent]:
pop through the parent-child edge to obtain one vertex-biconnected block
vertices = distinct endpoints of popped edges
if number_of_edges == number_of_vertices:
add every block edge to answer
sort and output answer edge IDs
完整 GNU C++17 参考实现见附件:CodeForces-962F.cpp
识别信号是“边属于多少个简单环”,它比单纯判桥更细,需要看边所在点双块的整体结构。若图允许重边,两个点两条平行边是否按题目定义构成长度 2 环必须先确认,不能直接沿用简单图结论。
对每个简单连通无向图 $G$,问是否存在 $0\le X_v\le\deg(v)$,使得无论怎样给边定向,都至少有一个顶点 $v$ 同时不满足 $\operatorname{in}(v)=X_v$ 与 $\operatorname{out}(v)=X_v$。测试总 $\sum N,\sum M\le2\times10^5$。
量词顺序必须写清:我们先选整组 $X$,对手再尝试找一种方向;题目询问能否让对手对所有方向都失败。
官方结论的反面更容易判定:不存在题目所求 $X$,当且仅当:
必要性:若图有奇环,取 $X=0$ 即可让所有定向失败。若固定 $v$ 的组大小无法拼出某个 $x$,取 $X_v=x$、其余为 0;其余点要满足 0,就必须成为源或汇。在一个点双块里,沿偶环传播后,$v$ 的同组边只能整组入或整组出,于是 $x$ 不可实现。
充分性:先看树。根到叶处理,每个点只有父边方向已定,其余子边可补出任意要求的入度或出度。一般图的块割树仍是一棵树;把一个点双块视为树上的“粗边”。原图二分保证块内可按两侧统一源汇定向,而条件 2 保证在割点处能选若干整组凑出所需数量,于是对任意 $X$ 都能逐块构造一种满足方向,题目所求的反例 $X$ 不存在。
排序后,组大小能覆盖所有 $[0,\sum c_i]$ 的充要条件是:初始 $S=0$,每次有 $c_i\le S+1$,再令 $S\leftarrow S+c_i$。若 $c_i>S+1$,数 $S+1$ 无法表示;反之新区间 $[0,S]$ 与 $[c_i,c_i+S]$ 无缝相接。点双分解 $O(N+M)$,所有组排序总计 $O(M\log M)$。
bipartite = two_color(G)
blocks = vertex_biconnected_edge_blocks(G) // Tarjan edge stack
for each block:
count how many popped edges touch each incident vertex v
append that count to groups[v]
complete = True
for each vertex v:
sort(groups[v]); covered = 0
for c in groups[v]:
if c > covered + 1: complete = False
covered += c
answer = not (bipartite and complete)
点双块中的 $c$ 是关联边数而非块的顶点数。连通简单图仍应按边编号维护父边;附件用迭代 DFS,避免链图深度 $2\times10^5$ 爆栈。完整 GNU C++17 实现:AtCoder-agc071_c.cpp
识别信号是“方向选择在环中传播、但在割点处分组独立”。若要求实际输出那组 $X$,在发现奇环时可取全 0;在覆盖断点 $S+1$ 时令该点取 $S+1$、其余取 0。若图允许重边,二边环也属于点双结构,父边必须按边号跳过。
low 判据只差等号,却对应不同删除模型。Simple Cycles Edges 中 $E_c=V_c$ 为什么推出块是一个环?先对拍割点:随机删点比较剩余连通块数。再手画“两个环共一个点”和“两个环共一条边”的图,分别列出点双块。第二题卡住时按 Hint 逐级展开,复盘重点是从“恰一个环”转成“点双块的边数等于点数”。