Tarjan 全家桶

Tarjan 算法的原理是通过维护每个点的 dfs 序和最小回溯值 low 来判断图的性质。


割点

定义:删去后图的联通分量增加,即图的连通性被破坏的点。

按是否是根节点讨论。

非根节点的:若 lowudfnu,则该点为割点。子树内所有点无法回溯到该点以前的点,那么删去该点子树和原图分离,故该点为割点。

根节点:有两个及以上子节点。比较显然。

注意:孤立点不算作割点。

点击查看代码
    /*割点*/
    int iscut[N];
    inline void Wtarjan(int u, int fa)
    {
        dfn[u] = low[u] = ++dt; int son = 0;
        for(int i = hh[u]; i != -1; i = ne[i])
        {
            int v = to[i];
            if(v == fa) continue;
            if(!dfn[v])
            {
                son++;
                Wtarjan(v, u);
                low[u] = min(low[u], low[v]);
                if(low[v] >= dfn[u] && fa) iscut[u] = 1;
            }
            else low[u] = min(dfn[v], low[u]);
        }
        if(!fa && son >= 2) iscut[u] = 1;
    }

点双联通分量

定义:删去任意一个点,不改变该分量连通性的极大连通子图。

更易懂的解释为:不含割点的极大图。

性质:

  • 两个点双至多存在一个公共点(该点一定是割点);

  • 任意一个非割点的点都只存在于一个点双中,割点一定属于两个及以上的点双;

  • 对于一个点双(除去孤立边),其内部任意两不同点一定存在两条及以上的点不重复的路径。

那么我们找到割点,也就找到了点双。

实现:实时维护一个栈,若 lowvdfnu,则将栈内 v 及以上的点全部弹出作为一个点双,随后将 u 加入点双。

注意:孤立点算作点双。

点击查看代码
    /*点双*/
    int num, st[N], top;
    vector<int> ans[N];
    inline void Wtarjan(int u, int fa)
    {
        dfn[u] = low[u] = ++dt; int son = 0; st[++top] = u;
        for(int i = hh[u]; i != -1; i = ne[i])
        {
            int v = to[i];
            if(v == fa) continue;
            if(!dfn[v])
            {
                son++;
                Wtatjan(v, u);
                low[u] = min(low[u], low[v]);
                if(low[v] >= dfn[u])
                {
                    ++num;
                    while(top)
                    {
                        ans[num].P_B(st[top--]);
                        if(st[top + 1] == v) break;
                    }
                    ans[num].P_B(u);
                }
            }
            else low[u] = min(low[u], dfn[v]);
        }
        if(!fa && !son) ans[++num].P_B(u);
    }

割边(桥)

定义:删去后图的联通分量增加,即图的连通性被破坏的边。

实现:若一条边为 dfs 树上的边,且 lowv>dfnu,则该边为割边。子树上所有点都无法回溯至该点及以上的点,删去后子树与原图分离,故该边为割边。

注意:链式前向星存图若用 ii^1 来代表一条边的双向,下标需从 2 开始。求割边强制要求判父亲,及不能用 dfs 到该点的反向边更新回溯值。孤立边是割边。

点击查看代码
    /*割边*/
    // 边下标从 2 开始
    bool isbridge[N << 1];
    inline void Wtarjan(int u, int fa)
    {
        dfn[u] = low[u] = ++dt;
        for(int i = hh[u]; i != -1; i = ne[i])
        {
            int v = to[i];
            if(v == fa) continue; // necessary
            if(!dfn[v])
            {
                Wtarjan(v, u);
                low[u] = min(low[u], low[v]);
                if(low[v] > dfn[u]) isbridge[i] = isbridge[i ^ 1] = 1;
            }
            else low[u] = min(low[u], dfn[v]);
        }
    }

边双连通分量

定义:删去任意一条边,不改变该分量连通性的极大连通子图。即不存在割边的极大联通子图。

实现:先求割边,将割边删去后,每个连通块为一个边双。

点击查看代码
    /*边双*/
    // 先求割边,将割边删去,剩下的即为边双
    bool yz[N];
    vector<int> ans[N];
    inline void Wtarjan(int u, int now)
    {
        yz[u] = 1; ans[now].P_B(u);
        for(int i = hh[u]; i != -1; i = ne[i])
        {
            int v = to[i];
            if(yz[v] || isbridge[i]) continue;
            Wtarjan(v, now);
        }
    }

强连通分量

更常见的形式为缩点。主要存在于有向图。

定义:任意两点相互可达的极大连通子图。

实现:维护一个栈,若一个点遍历完子树后仍有 lowu=dfnu,则栈内 u 及以上的点构成一个强联通分量,可缩为一个点。详细原理见此

点击查看代码
    /*强联通分量(缩点)*/
    int num, bl[N], st[N], top;
    inline void Wtarjan(int u, int fa)
    {
        dfn[u] = low[u] = ++dt; st[++top] = u;
        for(int i = hh[u]; i != -1; i = ne[i])
        {
            int v = to[i];
            if(v == fa) continue;
            if(!dfn[v])
            {
                Wtarjan(v, u);
                low[u] = min(low[u], low[v]);
            }
            else if(!bl[v]) low[u] = min(low[u], dfn[v]);
        }
        if(low[u] == dfn[u])
        {
            ++num;
            while(st[top] != u) bl[st[top--]] = num;
            top--, bl[u] = num;
        }
    }

完整实现

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
#define P_B(x) push_back(x)

namespace Wisadel
{ // Tarjan 全家桶 
    int hh[N], to[N << 1], ne[N << 1], cnt;
    int dfn[N], dt, low[N];
    inline void Wadd(int u, int v){to[++cnt] = v; ne[cnt] = hh[u]; hh[u] = cnt;}

    /*割点*/
    int iscut[N];
    inline void Wtarjan(int u, int fa)
    {
        dfn[u] = low[u] = ++dt; int son = 0;
        for(int i = hh[u]; i != -1; i = ne[i])
        {
            int v = to[i];
            if(v == fa) continue;
            if(!dfn[v])
            {
                son++;
                Wtarjan(v, u);
                low[u] = min(low[u], low[v]);
                if(low[v] >= dfn[u] && fa) iscut[u] = 1;
            }
            else low[u] = min(dfn[v], low[u]);
        }
        if(!fa && son >= 2) iscut[u] = 1;
    }

    /*点双*/
    int num, st[N], top;
    vector<int> ans[N];
    inline void Wtarjan(int u, int fa)
    {
        dfn[u] = low[u] = ++dt; int son = 0; st[++top] = u;
        for(int i = hh[u]; i != -1; i = ne[i])
        {
            int v = to[i];
            if(v == fa) continue;
            if(!dfn[v])
            {
                son++;
                Wtatjan(v, u);
                low[u] = min(low[u], low[v]);
                if(low[v] >= dfn[u])
                {
                    ++num;
                    while(top)
                    {
                        ans[num].P_B(st[top--]);
                        if(st[top + 1] == v) break;
                    }
                    ans[num].P_B(u);
                }
            }
            else low[u] = min(low[u], dfn[v]);
        }
        if(!fa && !son) ans[++num].P_B(u);
    }

    /*割边*/
    // 边下标从 2 开始
    bool isbridge[N << 1];
    inline void Wtarjan(int u, int fa)
    {
        dfn[u] = low[u] = ++dt;
        for(int i = hh[u]; i != -1; i = ne[i])
        {
            int v = to[i];
            if(v == fa) continue; // necessary
            if(!dfn[v])
            {
                Wtarjan(v, u);
                low[u] = min(low[u], low[v]);
                if(low[v] > dfn[u]) isbridge[i] = isbridge[i ^ 1] = 1;
            }
            else low[u] = min(low[u], dfn[v]);
        }
    }

    /*边双*/
    // 先求割边,将割边删去,剩下的即为边双
    bool yz[N];
    vector<int> ans[N];
    inline void Wtarjan(int u, int now)
    {
        yz[u] = 1; ans[now].P_B(u);
        for(int i = hh[u]; i != -1; i = ne[i])
        {
            int v = to[i];
            if(yz[v] || isbridge[i]) continue;
            Wtarjan(v, now);
        }
    }

    /*强联通分量(缩点)*/
    int num, bl[N], st[N], top;
    inline void Wtarjan(int u, int fa)
    {
        dfn[u] = low[u] = ++dt; st[++top] = u;
        for(int i = hh[u]; i != -1; i = ne[i])
        {
            int v = to[i];
            if(v == fa) continue;
            if(!dfn[v])
            {
                Wtarjan(v, u);
                low[u] = min(low[u], low[v]);
            }
            else if(!bl[v]) low[u] = min(low[u], dfn[v]);
        }
        if(low[u] == dfn[u])
        {
            ++num;
            while(st[top] != u) bl[st[top--]] = num;
            top--, bl[u] = num;
        }
    }
}
signed main(){}

未经运行检测,如果哪写挂了记得告诉我啊(


祝大家 NOIP rp++!

posted @   Ratio_Y  阅读(198)  评论(7编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示