【笔记】Tarjan算法

dfs生成树

对一张图进行 dfs,我们会得到包含一大堆非树边的“树”。
这棵树上除了正常的树边(父→子),还有:

  1. 返祖边:\(u \to v\ (v \in {\rm Anc}(u))\) ;
  2. 前向边:\(u \to v\ (v \in {\rm Subtree}(u))\) ;
  3. 横叉边:\(u \to v\ ({\rm vis}(v) = {\rm true} \land v \notin {\rm Anc}(u) \cup {\rm \rm Subtree}(u))\) .
    dfs 的过程中,Tarjan 算法维护了这样两个变量数组:
  4. \({\rm dfn}_u\):dfs 序(先序);
  5. \({\rm low}_u\)\(u\) 的子树中能回溯到的最早的节点,形式化地,\({\rm low}_u = \min\limits_{v \in {\rm Subtree}(u) \cup {\rm To}(u)}\{{\rm dfn}_v\}\) .

强连通分量(Strongly Connected Components)

\(\rm Lm.\) 如果节点 \(u\) 是某个强连通分量在搜索树上的第一个结点(\(\rm dfn\) 序最小的),则这个强连通分量在搜索树以 \(u\) 为根的子树中。

证:因为 \(\rm dfn\) 序最小嘛……

\(\rm Th.\) 如果 \({\rm dfn}_u = {\rm low}_u\),则 \(u\) 和栈中在 \(u\) 上方的节点构成一个强连通分量。(这就是上面引理中的那“第一个”节点)

\(\rm E.g.\) B3609 [图论与代数结构 701] 强连通分量

void dfs(int u) {
    dfn[u] = low[u] = ++dfs_cnt;
    in_stk[stk[++top] = u] = true;
    for(int v : to[u]) {
        if(!dfn[v]) {                   //树边
            dfs(v);
            low[u] = min(low[u],low[v]);
        } else if(in_stk[v])            //非树边
            low[u] = min(low[u],dfn[v]);
    }
    if(dfn[u] == low[u]) {
        ++sc;
        do {
            blg[stk[top]] = sc;
            ++sz[sc];
            in_stk[stk[top]] = false;
        } while(stk[top--] != u);
    }
}

割点与桥

P3388 【模板】割点(割顶)
\(\rm Th.\) 如果对于一个节点 \(u\),它的一个子节点 \(v\) 不能回溯到之前的节点,则 \(u\) 为割点。形式化地,\({\rm low}_v \ge {\rm dfn}_u\)

void dfs(int u,int fa) {
    dfn[u] = low[u] = ++dfs_cnt;
    int ch_cnt = 0;
    for(int v : to[u]) {
        if(!dfn[v]) {
            ++ch_cnt;
            dfs(v,u);
            low[u] = min(low[u],low[v]);
            if(low[v] >= dfn[u]&&!cutv[u]) {
                cutv[u] = true;
                ++cutv_tot;
            }
        } else if(fa != v)
            low[u] = min(low[u],dfn[v]);
    }
    if(fa == u&&ch_cnt <= 1&&cutv[u]) {
        cutv[u] = false;
        --cutv_tot;
    }
}

值得注意的是,对于 dfs 的起点,只有一个子节点时必不为割点,除去这种情况。
\(\rm Th.\) 在一个简单图中,对于一个节点 \(u\) 及其搜索树中的子节点 \(v\),若 \({\rm low}_v > {\rm dfn}_u\),则边 \(\{u,v\}\) 为割边。
这个跟割点几乎就是一样的,还不用考虑根节点(搜索起点)

边双连通分量

P8436 【模板】边双连通分量
\(\rm Th.\) 把无向边拆成两个有向边,新的有向图中的强连通分量是原无向图中的边双连通分量

void dfs(int u,int fa) {
    dfn[u] = low[u] = ++dfs_cnt;
    in_stk[stk[++top] = u] = true;
    bool to_fa = false;
    for(int v : to[u]) {
        if(v == fa&&!to_fa) {
            to_fa = true;
            continue;
        }
        if(!dfn[v]) {
            dfs(v,u);
            low[u] = min(low[u],low[v]);
        } else if(in_stk[v])
            low[u] = min(low[u],dfn[v]);
    }
    if(dfn[u] == low[u]) {
        ++sc;
        do {
            blg[stk[top]] = sc;
            ++sz[sc];
            in_stk[stk[top]] = false;
        } while(stk[top--] != u);
    }
}

注意到:布尔变量 to_fa,我们排除掉了去往父节点的边(因为本来是无向图,这俩是一条边),但是还要考虑重边的存在。
当然,我们还有一种选择,就是去掉所有桥后找连通分量

点双连通分量

P8435 【模板】点双连通分量
\(\rm Th.\) 两个点双连通分量的分界是割点,割点属于两个点双连通分量。

void dfs(int u,int fa) {
    dfn[u] = low[u] = ++dfs_cnt;
    stk[++top] = u;
    if(u == fa&&to[u].size() == 0) {
        vcc[++vcc_cnt].push_back(u);
        return;
    }
    int ch_cnt = 0;
    for(int v : to[u]) {
        if(!dfn[v]) {
            dfs(v,u);
            low[u] = min(low[u],low[v]);
            if(low[v] >= dfn[u]) {
                if(++ch_cnt > 1||u != fa)
                    cutv[u] = true;
                ++vcc_cnt;
                do vcc[vcc_cnt].push_back(stk[top--]);
                while(stk[top+1] != v);
                vcc[vcc_cnt].push_back(u);
            }
        } else
            low[u] = min(low[u],dfn[v]);
    }
}

如果搜索树只有一个结点的话,这个节点单成一个点双。

差分算法

posted @ 2025-04-28 23:24  noaL02d  阅读(12)  评论(0)    收藏  举报