Tarjan算法与无向图连通性

无向图的割点与桥

在看Tarjan算法之前先看以下概念:
给定无向图连通图G=(V,E)
若对于 x∈V ,从图中删除节点x以及与x关联的所有边之后,G 分裂为两个或者以上的连通块,则称节点 x 为无向连通图 G 的割点
若对于 e∈E ,删除边 e 之后,G 分裂为两个不相连的子图,则称 eG割边
Tarjan算法能够在线性的时间内求出无向图的割点和桥。

时间戳
在图的dfs过程中,按照每个节点被第一次访问的时间顺序,依次给 N 个节点 1~N 的整数标记,这个标记就是时间戳,节点 x 的时间戳记录为 dfn[x]。假设 x 节点 dfn[x]=3,意思就是在dfs过程中,访问到x节点之前有两个节点被访问过,x节点是第3个被访问到的节点。

搜索树
在无向连通图中,以任意一个节点为根节点进行dfs,每个节点访问一次,所有经过的边与点构成的树称为搜索树。下图给出一颗搜索树,其中灰色的点为选择的根节点,蓝色的边为dfs所经过的边,右图就是所构成的搜索树,节点上的编号是对应的时间戳。
请添加图片描述

追溯值
除了时间戳以外,Tarjan算法还引入了追溯值 low[x] 的概念。设subtree(x)为搜索树中以x为根节点的子树。low[x] 定义为以下节点集合时间戳的最小值。
1.subtree(x)中的节点。
2.与subtree(x)中的节点通过某一条边不在搜索树上的边相连的节点。
根据定义,为了计算 low[x],应该先令 low[x]=dfn[x],然后dfs的过程,考虑从x出发的每条无向边边(x,y):
如果无向边在搜索树上,则 low[x]=min(low[x],low[y]).
如果无向边不在搜索树,则 low[x]=min(low[x],dfn[y])

割边判定法则
无向边(x,y)是割边,当且仅当搜索树上存在一个节点x和它的子节点y,满足 dfn[x]<low[y]
我们来看一下 dfn[x]>=low[y] 会是什么情况,如果 dfn[x]>=low[y] ,说明以节点 y 为根节点的子树中的某个节点经过某条不在搜索树的边回到了 x 或者x之前的点,那么无向边(x,y)必定参与形成了某个环,反之 dfn[x]<low[y],说明节点y无法通过无向边(x,y)以外的任何一条路径回到节点x或节点x之前的节点,那么说明要从节点x到它的子节点y只有无向边(x,y)这一条,所以这一条边一定为割边。

根据上面追溯值和时间戳的概念再加上割边判定法则,我们可以有如下判断割边的Tarjan算法:以某个节点为根节点dfs搜索,在计算时间戳dfn和追溯值low的时候,对于搜索时的某条边(x,y)在回溯的时候dfn[x]<low[y]说明这条边为割边,说的可能不太清除具体看如下代码示例:

#include<bits/stdc++.h>
using namespace std;
const int maxn=1000005;
const int inf=0x3f3f3f3f;
int n,m;//节点数n和边数m
int tot;
int head[1005],Next[maxn],ver[maxn],bridge[maxn],w[maxn];
//bridge[x]如果为1说明边x为割边
int dfn[1005],low[1005];
int deep;//记录目前节点访问顺序
void init()//初始化
{
    for(int i=1;i<=n;i++)
    head[i]=0;
    memset(bridge,0,sizeof(bridge));
    memset(dfn,0,sizeof(dfn));
    tot=1;
    deep=0;
}

void add(int x,int y,int z)//链式前向星添加边
{
    ver[++tot]=y,w[tot]=z,Next[tot]=head[x],head[x]=tot;
}

void tarjan(int x,int in_edge)//tarjan算法求割边
{
    dfn[x]=low[x]=++deep;//求当前节点的时间戳
    for(int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        if(!dfn[y])//如果节点y没有被访问过
        {
            tarjan(y,i);
            low[x]=min(low[x],low[y]);//dfs回溯的时候更新low[x]

            if(dfn[x]<low[y])//割边判定法则
            {
                bridge[i]=bridge[i^1]=1;
            }
        }else if(i!=(in_edge^1))//节点y不是x的父节点
        {
            low[x]=min(low[x],dfn[y]);
        }
    }
}
int main()
{
    while(~scanf("%d%d",&n,&m)&&(m||n))//输入节点数n,边数m
    {
        init();
        for(int i=1;i<=m;i++)//输入边
        {
            int x,y,z;
            scanf("%d%d%d",&x,&y,&z);
            add(x,y,z);
            add(y,x,z);
            //tot从2开始存储边,对于某个边i,边i^1为边i的反方向边
            //比如第2条边为(x,y),第2^1条边为(y,x)
        }

        int num=0;
        int f=0;
        int ans=inf;
        for(int i=1;i<=n;i++)
        {
            if(!dfn[i])
            {
                num++;//连通块个数
                tarjan(i,0);
            }
        }
        
        for(int i=2;i<=2*m;i+=2)//输出割边
        {
            if(bridge[i])
            {
                //tot从2开始存储边,对于某个边i,边i^1为边i的反方向边
                //比如第2条边为(x,y),第2^1条边为(y,x)
                printf("%d %d\n",ver[i^1],ver[i]);
            }
        }
    }
return 0;
}

模板题hdu4738

割点判定法则
如果x不是搜索树的根节点(dfs的起点),则x为割点当且仅当搜索树上存在一个节点 x 和它的子节点 y 满足如下条件:

dfn[x] <= low[y]

特别的,如果 x 是搜索树的根节点,则 x 是割点当且仅当搜索树上存在至少两个节点 y1y2x 的子节点,并且满足上述条件。

对于非根节点的节点x,满足上述条件,当dfn[x]<low[y]时,无向边(x,y)为割边,所以x一定为割点,如果dfn[x] == low[y]时说明,搜索树中以y为根节点的子树中,能通过某条不在搜索树的边回到节点x,但是回不到x以前的节点,所以删除节点x和与x关联的所有边之后,x之前的节点就没办法通过其他边到达y了。dfn[x]>low[y] 的时候说明搜索树中以y为根节点的子树中,能通过某条不在搜索树的边e回到x之前的节点,那么删除节点x和与x关联的所有边之后,x之前的节点还是能通过那条边e到达y。
所以对于非根节点的节点 x 满足 dfn[x]<=low[y] 的条件说明x是割点。

对于根节点 x ,它的两个子节点 y1y2dfn[x] <= low[y1]&&dfn[x] <= low[y2] 因为在搜索树上,主要看y1y2 同时为 x 的子节点这个条件,dfn[x] <= low[y1]&&dfn[x] <= low[y2] 这个条件其实也就是这个意思,因为x为根节点,所以 dfn[x] 肯定是1,是最小的,那么对于任何节点y都有low[y]>=dfn[x],所以其实dfn[x] <= low[y1]&&dfn[x] <= low[y2] 这个条件就是搜索树上根节点x至少有两个子节点。搜索树上为根节点的字节点的两个节点一定要通过x这个节点才能够互相到达,所以当根节点x在搜索树上有两个子及以上的子节点时根节点为割点。代码示例如下:

#include<bits/stdc++.h>
using namespace std;
const int maxn=2*1e4+10;
const int maxm=2*1e5+10;
int n,m;//节点数n,边数m
int tot=0;
int head[maxn],Next[maxm],ver[maxm];
int dfn[maxn],low[maxn];
int ans[maxn];//答案
int cut[maxn];//cut[x]=1说明x为割点
int deep=0;//当前时间戳
int root=0;//根节点

void add(int x,int y)
{
    ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
}
void tarjan(int x)
{
    dfn[x]=low[x]=++deep;
    int sub_root=0;//子节点个数
    for(int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        if(!dfn[y])
        {
            sub_root++;
            tarjan(y);
            low[x]=min(low[x],low[y]);

            if(low[y]>=dfn[x])
            {
                if(root!=x||sub_root>1)//root==x的情况下在搜索树中根节点有两个子节点才算割点
                cut[x]=1;
            }
        }else//因为dfn[x]<=low[y]时判断x为割点,所以不需要管这条便是不是入边的反向边了
        low[x]=min(low[x],dfn[y]);
    }
}
int main()
{
    int num=0;//割点个数

    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);
        add(y,x);
    }

    for(int i=1;i<=n;i++)
    {
        if(!dfn[i])//tarjan图不一定联通的情况下
        {
            root=i;
            tarjan(i);//求割点
        }
    }

    for(int i=1;i<=n;i++)
    {
        if(cut[i])//cut[i]为1说明i为割点
        ans[++num]=i;
    }

    printf("%d\n",num);//割点个数
    if(num)
    {
        for(int i=1;i<=num;i++)
        {
            if(i!=num)
            printf("%d ",ans[i]);
            else
            printf("%d\n",ans[i]);
        }
    }
    

return 0;
}

模板题洛谷割点模板题P3388

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值