学完后,你应能从“互相可达”识别强连通分量(Strongly Connected Component, SCC),推导 Tarjan 的栈不变量,并在缩点 DAG 上解决选点、可达终点和最少补边问题。
前置:有向图 DFS、栈、DAG 入度/出度。
low?对每个点做一次可达性 DFS 需要 $O(V(V+E))$。但许多题并不关心一个强连通区域内部怎样绕行:区域内任一点能承担相同的可达角色。若先把互相可达的点压成一个点,任意有向图就变成 DAG,入度、出度和拓扑结构才真正显露。
令 dfn[u] 为 $u$ 首次访问时间,low[u] 为从 $u$ 的 DFS 子树出发、只经过仍未封闭的点,能够回到的最小 dfn。
low[v] 更新 low[u];dfn[v] 更新;为什么 low[u]==dfn[u] 时可以弹栈?此时 $u$ 的子树无法回到 $u$ 之前仍未封闭的祖先;从栈顶弹到 $u$ 的所有点都能沿 DFS 树回溯关系回到 $u$,且从 $u$ 能沿树边到它们,正好形成一个极大的互相可达集合。
停顿:无向图求桥时也叫
low,含义是否完全相同?
Tarjan 始终维护:栈中恰是已经访问但尚未归入 SCC 的点。每次发现根 low[u]==dfn[u],弹出的集合内部互相可达;集合不能再加入更早或栈外点,否则 low[u] 会更小或那个 SCC 尚未封闭,产生矛盾。每点入栈、出栈各一次,每边扫描一次,时间 $O(V+E)$,空间 $O(V+E)$。
缩点时对原边 $u\to v$,若 scc[u] != scc[v],就在分量间加边。是否去重取决于后续计算:只判断入度是否为零时可不去重;若统计边数或做 DP,重复边可能改变语义。
DFS(u):
dfn[u] = low[u] = next_timestamp()
push u into Tarjan stack; mark u in_stack
for v in graph[u]:
if v is unvisited:
DFS(v); low[u] = min(low[u], low[v])
else if v is still in_stack:
low[u] = min(low[u], dfn[v])
if low[u] == dfn[u]:
pop through u; all popped vertices form one SCC
上面是语义模板,不是可直接编译片段。递归深度最坏为 $V$;当 $V$ 达到数十万时,应确认评测栈限制,必要时改迭代 Kosaraju/Tarjan,不能把递归崩溃误判成算法错误。
有 $n\le10^5$ 个路口、$m\le3\times10^5$ 条有向边,在点 $i$ 建检查站费用不超过 $10^9$。一个站能保护与它互相可达的点。求覆盖全部点的最小总费用,并在检查站数量也最少的前提下,求最优方案数模 $10^9+7$。
把图分成 SCC。每个 SCC 外的点都不可能与其中点互相可达,因此每个分量至少建一个站;在分量内选费用最小点即可保护全分量,并且为了检查站数最少恰选一个。总费用是各 SCC 最小费用之和,方案数是各 SCC 中最小费用点数量的乘积。
下界“每分量至少一个”与构造“每分量恰选一个最小点”相等,故费用与数量均最优。复杂度 $O(n+m)$;总费用使用 long long。
components = Tarjan_SCC(graph)
total_cost = 0; ways = 1
for component in components:
best = minimum(cost[v] for v in component)
total_cost += best
ways *= count(v in component where cost[v] == best) (mod MOD)
完整 GNU C++17 参考实现见附件:CodeForces-427C.cpp
若每个 SCC 要选至少 $k$ 个站,应取分量内前 $k$ 小并处理同价组合数;这时“只记录最小值与次数”不再够用。
$2\le N\le100$,有向边表示软件可从一所学校传给另一所。第一问:至少给多少所学校初始软件,才能传遍全网?第二问:至少新增多少条有向边,才能从任意学校出发传遍全网?
缩点得到 DAG。第一问必须在每个源 SCC 至少投放一次;从所有源出发能到达 DAG 的全部点,所以答案是源数。
第二问若已经只有一个 SCC,答案为 0。否则每个源至少要新增入边,每个汇至少要新增出边,故下界为两者最大值。把汇点与源点按可达关系适当排列,循环连接“第 $i$ 个汇到下一个源”,不足的一侧重复使用端点,即可用 max(source,sink) 条边让所有分量进入同一个环状可达结构,达到下界。
component = Tarjan_SCC(graph)
if there is only one component: output 1, 0
build condensation indegree and outdegree
sources = count(indegree == 0)
sinks = count(outdegree == 0)
output sources, max(sources, sinks)
完整 GNU C++17 参考实现见附件:POJ-1236.cpp
第二问只要求最少数量,不要求实际输出补边;若要输出边,不能随意把任意汇与任意源循环相连而不证明整体强连通,需要构造与可达覆盖配合。
有 $N\le10^4$ 头牛与 $M\le5\times10^4$ 条有向“认可”关系,可达性具有传递含义。求被其他所有牛认可的牛数。
若缩点 DAG 有多个汇 SCC,它们互相不可达,不可能存在被所有点到达的牛。若只有一个汇 SCC,则从任一分量沿出边最终必须落到某个汇,而汇唯一,所以所有分量都能到达它;其中每头牛均满足要求。答案因此是唯一汇 SCC 的大小,否则为 0。
component = Tarjan_SCC(graph)
compute every condensation component's size and outdegree
sink_components = components with outdegree 0
if len(sink_components) != 1: output 0
else: output size of the unique sink component
完整 GNU C++17 参考实现见附件:POJ-2186.cpp
“所有点能到我”对应唯一汇 SCC;“我能到所有点”对应唯一源 SCC。方向翻转是本类题最常见的失误。
dfn、low、in_stack 各表示什么?dfn[v],遇到已出栈点不更新?先手写 Tarjan 并用暴力可达矩阵核对随机小图的分组;再依次阅读三题。Checkposts 应独立实现,Network of Schools 重点复述补边下界,Popular Cows 要刻意做一次“源/汇方向”反例。