Tarjan算法与无向图连通性(割点\割边\双连通分量\缩点)

T a r j a n Tarjan Tarjan算法与无向图连通性

无向图的割点与桥:

给定无向连通图 G = ( V , E ) G=(V,E) G=(V,E)
若对于 x ∈ V x\in V xV,从图中删去节点 x x x以及所有与 x x x关联的边之后, G G G分裂成两个或两个以上不相连的子图,则称 x x x G G G的割点。
若对于 e ∈ E e\in E eE,从图中删去边 e e e之后, G G G分裂成两个不相连的子图,则称 e e e G G G的桥或割边。

时间戳:

在图的深度优先遍历过程中,按照每个节点第一次被访问的时间顺序,给予 N N N个节点 1 − N 1-N 1N的整数标记。这些标记就叫做时间戳。(其实和 d f s dfs dfs序是一个东西)

搜索树:

在无向连通图中任选一个节点出发进行深度优先遍历,每个点只访问一次。所有发生递归的边 ( x , y ) (x,y) (x,y)(换言之,从 x x x y y y是对 y y y的第一次访问)构成一棵树,我们把它称为"无向连通图的搜索树"。如下图所示:

追溯值:

除了时间戳以外, T a r j a n Tarjan Tarjan算法还引入了一个"追溯值" l o w [ x ] low[x] low[x]。设 s u b t r e e ( x ) subtree(x) subtree(x)表示搜索树中以 x x x为根的子树。 l o w [ x ] low[x] low[x]定义为以下节点的时间戳的最小值:
1. s u b t r e e ( x ) subtree(x) subtree(x)中的节点。
2.通过 1 1 1条不在搜索树上的边,能够到达 s u b t r e e ( x ) subtree(x) subtree(x)的节点。
以上图为例,假设时间戳就是节点编号。 s u b t r e e ( 2 ) = { 2 , 3 , 4 , 5 } subtree(2)=\{2,3,4,5\} subtree(2)={2,3,4,5},另外节点 1 1 1通过不在搜索树上的边 ( 1 , 5 ) (1,5) (1,5)能够到达 s u b t r e e ( 2 ) subtree(2) subtree(2)。所以 l o w [ 2 ] = 1 low[2]=1 low[2]=1
根据定义,为了计算 l o w [ x ] low[x] low[x],应该先令 l o w [ x ] = d f n [ x ] low[x]=dfn[x] low[x]=dfn[x],然后考虑从 x x x出发的每条边 ( x , y ) (x,y) (x,y)
若在搜索树上 x x x y y y的父节点,则令 l o w [ x ] = m i n ( l o w [ x ] , l o w [ y ] ) low[x]=min(low[x],low[y]) low[x]=min(low[x],low[y])
若无向边 ( x , y ) (x,y) (x,y)不是搜索树上的边,则令 l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x]=min(low[x],dfn[y]) low[x]=min(low[x],dfn[y])

割边判定法则:

无向边 ( x , y ) (x,y) (x,y)是桥,当且仅当搜索树上存在 x x x的一个子节点 y y y,满足: d f n [ x ] &lt; l o w [ y ] dfn[x]&lt;low[y] dfn[x]<low[y]根据定义, d f n [ x ] &lt; l o w [ y ] dfn[x]&lt;low[y] dfn[x]<low[y]说明从 s u b t r e e ( y ) subtree(y) subtree(y)出发,在不经过 ( x , y ) (x,y) (x,y)的前提下,不管走哪条边都无达到 x x x或者比 x x x更早访问的节点。若把 ( x , y ) (x,y) (x,y)删除,则 s u b t r e e ( y ) subtree(y) subtree(y)就形成了一个封闭的环境,因此 ( x , y ) (x,y) (x,y)是割边。反之,若不存在这样的子节点 y y y,使得 d f n [ x ] &lt; l o w [ y ] dfn[x]&lt;low[y] dfn[x]<low[y],则说明每个 s u b t r e e ( y ) subtree(y) subtree(y)都能绕行其他边到达 x x x或者比 x x x更早访问的节点, ( x , y ) (x,y) (x,y)自然就不是割边。
下面给出一个程序求出一张无向图中所有的桥,注意我们处理的是无向图,因此一条边要存储两次,用链式前向星存图的话同一条边存储的编号是: 2 , 3 、 4 , 5 、 6 , 7 2,3、4,5、6,7 2,34,56,7这样成对出现的,那么如果我们从 0 0 0号边进入了一个新的节点,就意味着 0 , 1 0,1 0,1号边都是搜索树上的边,不能用 d f n [ y ] dfn[y] dfn[y]更新 l o w [ x ] low[x] low[x]
求无向图中的所有桥:

#include<iostream>
#include<cstdio>
using namespace std;

const int maxn=1e5+5;

struct edge
{
    int to,nxt;
}Edge[maxn<<1];

bool bridge[maxn<<1];
int head[maxn],dfn[maxn],low[maxn];
int n,m,tot,num;

inline void addedge(int x,int y)
{
    Edge[++tot].to=y,Edge[tot].nxt=head[x],head[x]=tot;
}

void tarjan(int x,int in_edge)
{
    int y;
    dfn[x]=low[x]=++num;
    for(int i=head[x];i;i=Edge[i].nxt)
    {
        y=Edge[i].to;
        if(!dfn[y])//未访问过的节点
        {
            tarjan(y,i);
            low[x]=min(low[x],low[y]);
            if(low[y]>dfn[x]) //桥
                bridge[i]=bridge[i^1]=1;
        }
        else if(i!=(in_edge^1))//别忘了括号
            low[x]=min(low[x],dfn[y]);
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    int x,y;
    tot=1;
    for(int i=0;i<m;i++)
    {
        scanf("%d%d",&x,&y);
        addedge(x,y),addedge(y,x);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i,0);
    for(int i=2;i<tot;i+=2)
        if(bridge[i])
            printf("%d %d\n",Edge[i^1].to,Edge[i].to);
    return 0;
}

割点判定法则:

x x x不是搜索树的根节点(深度优先遍历的起点),则 x x x是割点当且仅当搜索树上存在 x x x的一个子节点 y y y,满足: d f n [ x ] &lt; = l o w [ y ] dfn[x]&lt;=low[y] dfn[x]<=low[y]

特别地,若 x x x是搜索树的根节点,则 x x x是割点当且仅当搜索树上至少存在两个子节点 y 1 , y 2 y_{1},y_{2} y1,y2满足上述条件(为什么是两个点?想一下当图为一条线时的情况)。证明方法与上面割边的情况类似,不再赘述。上面的那个例子中的两个割点是 1 1 1 6 6 6。且因为此处判别符号是 &lt; = &lt;= <=,因此不需要考虑父节点和重边的问题。
求无向图中所有的割点:

#include<iostream>
#include<cstdio>
using namespace std;

const int maxn=1e5+5;

struct edge
{
    int to,nxt;
}Edge[maxn<<1];

bool cut[maxn];
int head[maxn],dfn[maxn],low[maxn];
int n,m,tot,num,root;

inline void addedge(int x,int y)
{
    Edge[++tot].to=y,Edge[tot].nxt=head[x],head[x]=tot;
}

void tarjan(int x)
{
    int y,flag=0;
    dfn[x]=low[x]=++num;
    for(int i=head[x];i;i=Edge[i].nxt)
    {
        y=Edge[i].to;
        if(!dfn[y])//未访问过的节点
        {
            tarjan(y);
            low[x]=min(low[x],low[y]);
            if(low[y]>=dfn[x]) 
            {
                ++flag;
                if(x!=root||flag>=2)
                    cut[x]=1;
            }
        }
        else
            low[x]=min(low[x],dfn[y]);
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    int x,y;
    tot=1;
    for(int i=0;i<m;i++)
    {
        scanf("%d%d",&x,&y);
        addedge(x,y),addedge(y,x);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            root=i,tarjan(i);
    for(int i=1;i<=n;i++)
        if(cut[i])
            printf("%d ",i);
    return 0;
}

例题 洛谷 P3388
例题 BZOJ 1123

无向图的双连通分量:

若一张无向连通图不存在割点,则称它为"点双连通图"。若一张无向连通图不存在桥,则称它为"边双连通图"。无向图的极大点双连通子图称为"点双连通分量",简记为" v − D C C v-DCC vDCC"。无向图的极大边双连通子图称为"边双连通分量",简记为" e − D C C e-DCC eDCC",二者统称为"双连通分量",简记为" D C C DCC DCC"。
定理:
一张无相连通图是"点双连通图",当且仅当满足下列两个条件之一:
1.图的顶点数不超过2。
2.图中任意两点都同时包含在至少一个简单环中。简单环指的是不自交的环。
一张无向连通图是"边双连通图",当且仅当任意一条边都包含在至少一个简单环中。
证明略去。

边双连通分量( e − D C C e-DCC eDCC)的求法:

求出无向图中的所有桥,并把桥都删除后,图会分成若干个连通块,每一个连通块都是一个边双连通分量。如图:

因此在 t a r j a n tarjan tarjan求割边的算法之后,进行 d f s dfs dfs求连通块即可(不经过桥边)。

#include<iostream>
#include<cstdio>
using namespace std;

const int maxn=1e5+5;

struct edge
{
    int to,nxt;
}Edge[maxn<<1];

bool bridge[maxn<<1];
int head[maxn],dfn[maxn],low[maxn];
int n,m,tot,num;

inline void addedge(int x,int y)
{
    Edge[++tot].to=y,Edge[tot].nxt=head[x],head[x]=tot;
}

void tarjan(int x,int in_edge)
{
    int y;
    dfn[x]=low[x]=++num;
    for(int i=head[x];i;i=Edge[i].nxt)
    {
        y=Edge[i].to;
        if(!dfn[y])//未访问过的节点
        {
            tarjan(y,i);
            low[x]=min(low[x],low[y]);
            if(low[y]>dfn[x]) //桥
                bridge[i]=bridge[i^1]=1;
        }
        else if(i!=(in_edge^1))//别忘了括号
            low[x]=min(low[x],dfn[y]);
    }
}

int id[maxn],dcc;//dcc个边双连通分量
void dfs(int x)
{
    int y;
    id[x]=dcc;
    for(int i=head[x];i;i=Edge[i].nxt)
    {
        y=Edge[i].to;
        if(id[y]||bridge[i])
            continue;
        dfs(y);
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    int x,y;
    tot=1;
    for(int i=0;i<m;i++)
    {
        scanf("%d%d",&x,&y);
        addedge(x,y),addedge(y,x);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i,0);
    for(int i=1;i<=n;i++)
        if(!id[i])
            ++dcc,dfs(i);
    return 0;
}

e − D C C e-DCC eDCC的缩点:

把每个 e − D C C e-DCC eDCC看做一个节点,把桥边 ( x , y ) (x,y) (x,y)看作连接编号为 i d [ x ] id[x] id[x] i d [ y ] id[y] id[y] e − D C C e-DCC eDCC对应节点的无向边,会产生一棵树(若原来无向图不连通 则会产生森林)。这种把 e − D C C e-DCC eDCC收缩为一个节点的方法就称为"缩点"。

#include<iostream>
#include<cstdio>
using namespace std;

const int maxn=1e5+5;

struct edge
{
    int to,nxt;
}Edge[maxn<<1];

int head_sd[maxn];
edge sd[maxn<<1];//e-DCC 缩点后的树
int cnt;
bool bridge[maxn<<1];
int head[maxn],dfn[maxn],low[maxn];
int n,m,tot,num;

inline void addedge(int x,int y)//存图
{
    Edge[++tot].to=y,Edge[tot].nxt=head[x],head[x]=tot;
}

inline void addedge_sd(int x,int y)//e-Dcc 缩点后的树
{
    sd[++cnt].to=y,sd[cnt].nxt=head_sd[x],head_sd[x]=cnt;
}

void tarjan(int x,int in_edge)
{
    int y;
    dfn[x]=low[x]=++num;
    for(int i=head[x];i;i=Edge[i].nxt)
    {
        y=Edge[i].to;
        if(!dfn[y])//未访问过的节点
        {
            tarjan(y,i);
            low[x]=min(low[x],low[y]);
            if(low[y]>dfn[x]) //桥
                bridge[i]=bridge[i^1]=1;
        }
        else if(i!=(in_edge^1))//别忘了括号
            low[x]=min(low[x],dfn[y]);
    }
}

int id[maxn],dcc;//dcc个边双连通分量
void dfs(int x)
{
    int y;
    id[x]=dcc;
    for(int i=head[x];i;i=Edge[i].nxt)
    {
        y=Edge[i].to;
        if(id[y]||bridge[i])
            continue;
        dfs(y);
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    int x,y;
    tot=1;
    for(int i=0;i<m;i++)
    {
        scanf("%d%d",&x,&y);
        addedge(x,y),addedge(y,x);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i,0);
    for(int i=1;i<=n;i++)
        if(!id[i])
            ++dcc,dfs(i);
    cnt=1;
    for(int i=2;i<=tot;i++)
    {
        int x=Edge[i^1].to,y=Edge[i].to;
        if(id[x]==id[y])
            continue;
        addedge_sd(id[x],id[y]);
        addedge_sd(id[y],id[x]);
    }
    cout<<dcc<<endl;
    for(int i=2;i<cnt;i+=2)//可能会有重边
        cout<<sd[i^1].to<<' '<<sd[i].to<<endl;
    return 0;
}


点双连通分量( v − D C C v-DCC vDCC)的求法:

点双连通分量是一个很容易误解的概念。它与删除割点后图中剩余的连通块是不一样的。若某个节点为孤立点,则它自己单独构成一个 v − D C C v-DCC vDCC。除了孤立点之外,点双连通分量的大小至少为 2 2 2。虽然桥不属于任何 e − D C C e-DCC eDCC,但是割点可能属于多个 v − D C C v-DCC vDCC,看图:(割点为 1 、 6 1、6 16,有 4 4 4个点连通分量 粗线连接的点)

为了求出点双连通分量,需要在 t a r j a n tarjan tarjan算法中维护一个栈,并按照如下方法维护栈中的元素:
1.当一个节点第一次被访问时,该节点入栈。
2.当割点判定法则中的条件 d f n [ x ] &lt; = l o w [ y ] dfn[x]&lt;=low[y] dfn[x]<=low[y]成立时,无论 x x x是否为根,都要:
(1)从栈顶不断弹出节点,直至节点 y y y被弹出。
(2)刚才弹出的所有节点与节点 x x x一起构成一个 v − D C C v-DCC vDCC

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;

const int maxn=1e5+5;

struct edge
{
    int to,nxt;
}Edge[maxn<<1];

vector<int> dcc[maxn];//点双连通分量
bool cut[maxn];
int head[maxn],dfn[maxn],low[maxn],Stack[maxn];
int n,m,tot,num,root,top,cnt;

inline void addedge(int x,int y)
{
    Edge[++tot].to=y,Edge[tot].nxt=head[x],head[x]=tot;
}

void tarjan(int x)
{
    int y,flag=0;
    dfn[x]=low[x]=++num;
    Stack[++top]=x;
    if(x==root&&head[x]==0)//孤立点
    {
        dcc[++cnt].push_back(x);
        return ;
    }
    for(int i=head[x];i;i=Edge[i].nxt)
    {
        y=Edge[i].to;
        if(!dfn[y])//未访问过的节点
        {
            tarjan(y);
            low[x]=min(low[x],low[y]);
            if(low[y]>=dfn[x]) 
            {
                ++flag;
                if(x!=root||flag>=2)
                    cut[x]=1;
                cnt++;
                int z;
                do
                {
                    z=Stack[top--];
                    dcc[cnt].push_back(z);
                }while(z!=y);
                dcc[cnt].push_back(x);
            }
        }
        else
            low[x]=min(low[x],dfn[y]);
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    int x,y;
    tot=1;
    for(int i=0;i<m;i++)
    {
        scanf("%d%d",&x,&y);
        addedge(x,y),addedge(y,x);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            root=i,top=0,tarjan(i);
    for(int i=1;i<=cnt;i++)
    {
        cout<<"v-DCC #"<<i<<":";
        for(int j=0;j<dcc[i].size();j++)
            cout<<dcc[i][j]<<' ';
        cout<<endl;
    }
    return 0;
}
v − D C C v-DCC vDCC的缩点:

v − D C C v-DCC vDCC的缩点比 e − D C C e-DCC eDCC要复杂一些,因为一个割点可能属于多个 v − D C C v-DCC vDCC。设图中共有 p p p个割点和 t t t v − D C C v-DCC vDCC。我们建立一张包含 p + t p+t p+t个节点的新图,把每个 v − D C C v-DCC vDCC和每个割点都作为新图中的节点,并在每个割点与包含它的所有 v − D C C v-DCC vDCC之间连边,组成的新图依然是一棵树(森林)。

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;

const int maxn=1e5+5;

struct edge
{
    int to,nxt;
}Edge[maxn<<1];

int id[maxn];
int head_sd[maxn];
edge sd[maxn<<1];//缩点后的树
vector<int> dcc[maxn];//点双连通分量
bool cut[maxn];
int head[maxn],dfn[maxn],low[maxn],Stack[maxn];
int n,m,tot,num,root,top,cnt,tol;

inline void addedge(int x,int y)
{
    Edge[++tot].to=y,Edge[tot].nxt=head[x],head[x]=tot;
}

inline void addedge_sd(int x,int y)
{
    sd[++tol].to=y,sd[tol].nxt=head_sd[x],head_sd[x]=tol;
}

void tarjan(int x)
{
    int y,flag=0;
    dfn[x]=low[x]=++num;
    Stack[++top]=x;
    if(x==root&&head[x]==0)//孤立点
    {
        dcc[++cnt].push_back(x);
        return ;
    }
    for(int i=head[x];i;i=Edge[i].nxt)
    {
        y=Edge[i].to;
        if(!dfn[y])//未访问过的节点
        {
            tarjan(y);
            low[x]=min(low[x],low[y]);
            if(low[y]>=dfn[x]) 
            {
                ++flag;
                if(x!=root||flag>=2)
                    cut[x]=1;
                cnt++;
                int z;
                do
                {
                    z=Stack[top--];
                    dcc[cnt].push_back(z);
                }while(z!=y);
                dcc[cnt].push_back(x);
            }
        }
        else
            low[x]=min(low[x],dfn[y]);
    }
}

int new_id[maxn];//割点的新编号 从cnt+1开始

int main()
{
    scanf("%d%d",&n,&m);
    int x,y;
    tot=1;
    for(int i=0;i<m;i++)
    {
        scanf("%d%d",&x,&y);
        addedge(x,y),addedge(y,x);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            root=i,top=0,tarjan(i);
    num=cnt;
    for(int i=1;i<=n;i++)
        if(cut[i])
            new_id[i]=++num;
    tol=1;
    for(int i=1;i<=cnt;i++)
    {
        for(int j=0;j<dcc[i].size();j++)
        {
            int x=dcc[i][j];
            if(cut[x])
            {
                addedge_sd(i,new_id[x]);
                addedge_sd(new_id[x],i);
            }
            else
                id[x]=i;//除割点外 其他点仅属于1个v-DCC
        }
    }
    for(int i=2;i<tol;i+=2)
        cout<<sd[i^1].to<<' '<<sd[i].to<<endl;
    return 0;
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值