跳到主要内容

Tarjan 求割点和割边

Tarjan 可以在 O(n)O(n) 的时间复杂度内求出一个无向图中的割点、割边

求割点

在 Tarjan 的过程中,需要对每个节点 uu 维护以下变量:

黑色为树边,橙色为非树边;红字为 dfn\text{dfn}

  • DFS 序 dfnu\text{dfn}_udfn\text{dfn}前序更新的,一旦我们进入一个节点,就更新它的 dfn\text{dfn}
  • uu 出发、可以经过树边和非树边、不经过 uu 的父节点能到达的、DFS 序最小的节点的 DFS 序 lowu\text{low}_u。例如上图中 low2=4\text{low}_2=42132\to1\to3dfn3=4\text{dfn}_3=4)。low\text{low}后序更新的,我们从 uu 的子节点退出后,利用其信息更新 lowu\text{low}_u。(注意要把 lowu\text{low}_u 初始化为 dfnu\text{dfn}_u)具体来说,当我们 DFS 到一条边 (u,v)(u,v) 时:
    • 如果 vv 未访问过,说明它是子节点,于是递归搜索 vv。回溯时,lowumin{lowu,lowv}\text{low}_u\gets\min\{\text{low}_u,\text{low}_v\}。这是因为:子节点的 low\text{low} 不会“回头”,这样连下去的节点不会经过 uu 的父节点。于是子节点能连到的 low\text{low}uu 当然也能连到。

    • 如果 vv 访问过了,说明它不是子节点。lowumin{lowu,dfnv}\text{low}_u\gets\min\{\text{low}_u,\text{dfn}_v\}

我们判断某个点是否是割点的根据是:对于某个顶点 uu,如果存在一个儿子 vv,使得 lowvdfnu\text{low}_v \ge \text{dfn}_u,即不能回到祖先,那么 uu 点为割点。

例如上图中点 55dfn5=3\text{dfn}_5=3),容易看出其子树中所有节点的 low\text{low}>3\gt3,所以可以判断它是割点。

但是这个判据不适用于搜索起点 ss 就是割点的情况。请看下面的图(从 33 开始搜索):

我们发现,如果 ss 不是割点,那么能从 ss 的任意一个直接儿子出发,到达图中所有节点。所以,ss 只会“向下搜”一次。反之,如果 ss“向下搜”了 2\ge2 次,说明 ss 就是割点

注意

“不适用”的意思是:uu 为搜索起点时,即使满足判据也不是割点,需要另外的判据。

总结 & 编码

对于节点 uu,其是割点,当且仅当:

  • uu 不是搜索起点,且存在 uu 的一个儿子 vv,使得 lowvdfnu\text{low}_v\ge\text{dfn}_u
  • uu 是搜索起点,且 uu 在搜索树上只有一个儿子。

编码如下,请重点关注更新 low 的两行:

// 标记割点并统计割点数量
void tarjan(int rt, int fa = 0) {
node[rt].low = node[rt].dfn = node[fa].dfn + 1;
int son = 0;
for (int e = node[rt].head; e; e = edge[e].next) {
int v = edge[e].to;
if (!node[v].dfn) { // 没搜到过,是儿子
son++;
tarjan(v, rt);
node[rt].low = min(node[rt].low, node[v].low);
if (fa != 0 && !node[rt].isCut && node[v].low >= node[rt].dfn) {
node[rt].isCut = true, cnt++;
}
} else {
node[rt].low = min(node[rt].low, node[v].dfn);
}
}
if (fa == 0 && !node[rt].isCut && son > 1) {
node[rt].isCut = true, cnt++;
}
}

例题

求割边

方法和求割点非常类似。边 (u,v)(u,v) 是割边,当且仅当 lowv>dfnu\text{low}_v\gt\text{dfn}_u。(不需要考虑 uu 是根节点的问题)注意这里不能取等。

参考