• 「算法笔记」Tarjan 算法 割点和桥


    一、Tarjan 求割边

    桥(割边):无向连通图中,去掉一条边,图中的连通分量数增加,则这条边,称为 桥 或 割边

    判断桥:一条无向边 (x,y) 是桥,当且仅当搜索树上存在 x 的一个子节点 y,满足:dfn[x]<low[y]。(根据定义,由于 y 想要到达 x 的父亲必须经过 (x,y) 这条边,所以删去这条边,图不连通)。

    特别需要注意,因为遍历的是无向图,所以从每个点 x 出发,总能访问到它的父节点 fa。根据 low 的计算方法,(x,fa) 属于搜索树上的边,且 fa 不是 x 的子节点,故不能用 fa 的时间戳来更新 low[x]。

    如果仅记录每个节点的父节点,会无法处理重边的情况。当 x 与 fa 之间有多条边时,(x,fa) 一定不是桥。在这些重复的边中,只有一条算是“搜索树上的边”,其他的几条都不算。故有重边时,dfn[fa] 能用来更新 low[x]。

    改为 记录“递归进入每个节点的边的编号”。编号可认为是边在邻接表中存储的下标位置。把无向图的每一条边看做双向边,成对 存储在下标里。若沿着编号为 i 的边递归进入了节点 x,则忽略从 x 出发的编号 i xor 1 的边,通过其他边计算 low[x] 即可。

    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    const int N=1e5+5;
    int n,m,x,y,cnt=1,hd[N],to[N<<1],nxt[N<<1],dfn[N],low[N],num;    //注意 cnt 要初始化为 1。对于非负整数 n:当 n 为偶数时,n xor 1=n+1;当 n 为奇数时,n xor 1=n-1。因此,(0,1)(2,3)(4,5)... 关于 xor 1 运算构成“成对变换”。我们把无向图的正反方向的边分别存储在邻接表数组的第 n 与 n+1 位置(其中 n 为偶数)。由于 add 函数中写的是 ++cnt,并且我们刚刚所说的 n 为偶数,因此 cnt 要初始化为 1。 
    bool g[N<<1];
    void add(int x,int y){
        to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt;
    }
    void tarjan(int x,int fa){    //这里的 fa 不是 x 的父亲,是指递归进入每个节点的边的编号(边在邻接表中存储的下标位置),变量名是我瞎起的 qwq 
        dfn[x]=low[x]=++num;
        for(int i=hd[x];i;i=nxt[i]){
            int y=to[i];
            if(!dfn[y]){
                tarjan(y,i);
                low[x]=min(low[x],low[y]);
                if(low[y]>dfn[x]) g[i]=g[i^1]=1;
            }
            else if(i!=(fa^1)) low[x]=min(low[x],dfn[y]);
        }
    }
    signed main(){
        scanf("%lld%lld",&n,&m);
        for(int i=1;i<=m;i++){
            scanf("%lld%lld",&x,&y);
            add(x,y),add(y,x);
        }
        for(int i=1;i<=n;i++)
            if(!dfn[i]) tarjan(i,0);
        for(int i=2;i<cnt;i+=2)
            if(g[i]) printf("%lld %lld
    ",to[i^1],to[i]);    //输出的为割边 
        return 0;
    }

    二、Tarjan 求割点

    对于无向图 G,如果删除某个点 x 后,连通分量数目增加,则称点 x 是图 G 的割点。

    判断割点:若 x 不是搜索树的根结点(深度优先遍历的起点),则 x 是割点当且仅当搜索树上存在 x 的一个子节点 y,满足:dfn[x]≤low[y]。特别地,若 x 是搜索树的根结点,则 x 是割点当且仅当搜索树上出在至少两个子节点 y1,y2 满足上述条件。

    具体来说,我们可以把在结点 x 为根的子树(不包括 x)的点集即为 S(x),把不在 x 为根的子树中的点集即为 T(x)。对于某个点 x,若 S(x) 中存在至少一点 y,满足 y 与 T(x) 之间没有任何边,则 x 是割点。我们可以利用 Tarjan 算法记录 dfn[x] 和 low[x]。于是问题转化成,节点 x 是否存在一个儿子 y,使得 low[y]≥dfn[x]。

    我们只需一次 DFS 即可,总时间复杂度 O(n+m)。

    因为割点的判定是小于等于号,所以在求个割点时,不必考虑父节点和重边的问题,从 x 出发能访问到的所有点的时间戳都可以用来更新 low[x]。

    参考程序如下:

    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    const int N=1e5+5;
    int n,m,x,y,cnt=1,hd[N],to[N<<1],nxt[N<<1],dfn[N],low[N],num,root;
    bool g[N];
    void add(int x,int y){
        to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt;
    }
    void tarjan(int x){
        dfn[x]=low[x]=++num;
        int flag=0;
        for(int i=hd[x];i;i=nxt[i]){
            int y=to[i];
            if(!dfn[y]){
                tarjan(y),low[x]=min(low[x],low[y]);
                if(low[y]>=dfn[x]){
                    flag++;
                    if(x!=root||flag>1) g[x]=1;    //割点判定法则 
                }
            }
            else low[x]=min(low[x],dfn[y]);
        }
    }
    signed main(){
        scanf("%lld%lld",&n,&m);
        for(int i=1;i<=m;i++){
            scanf("%lld%lld",&x,&y);
            if(x==y) continue;
            add(x,y),add(y,x);
        }
        for(int i=1;i<=n;i++)
            if(!dfn[i]) root=i,tarjan(i);
        for(int i=1;i<=n;i++)
            if(g[i]) printf("%lld ",i);    //输出的是割点 
        return 0;
    }
  • 相关阅读:
    二级目录下的SESSION共享问题
    [Leetcode 65] 120 Triangle
    JMeter学习(一)工具简单介绍
    pycham破解方法——Mac上亲测成功
    jmeter目前发现一丢丢强大的功能(未完待续,需优化)
    jmeter实现请求返回参数利用到下一个请求中
    通用化case,拿走不谢——测试之路
    mac 下pip安装python三方库的时候提示 Could not fetch URL https://pypi.python.org/simple/virtualenv/: There was a problem confirming the ssl certificate:......
    sudo pip install MySQLdb找不到该资源的原因
    软件质量管理实践总结
  • 原文地址:https://www.cnblogs.com/maoyiting/p/12603018.html
Copyright © 2020-2023  润新知