Tarjan大法之强连通分量与割点和桥

· · 个人记录

天天膜tarjan,图论我很强。(✪ω✪)

记得noip2017前把塔老爷子当电脑桌面,然后,嗯......

就没有然后了。

掉到二队电脑每次关机就会清空桌面。 于是就坚定了我学好tarjan的志向。 好了,废话少说,开始我们的图论之旅。

我们先看一下它到底有什么鬼用。

求这个图中的强联通分量

先了解一些概念

有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。

如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。把每个强联通分量看成一个集合,把每个集合看成一个点,那么所有SCC就行成了一个DAG(有向无环图),就是著名的缩点。

比如下图

1、2、3能相互到达,所以他们就是一个强联通分量。

那怎么求呢?就要用到强大的tarjan算法。

tarjan算法:混合了dfs,玄学打标记,堆栈,搜索树等的奇葩又天才的算法(嗯,应该不是我发明的)

那么我们来看上一个图,嗯,自然而然地想到DFS,然后就行成了下面的搜索树

是不是有点头皮发麻了呢?一大坨线连过去连过来,我们怎么把他们分开形成一个个SCC呢?那就用要用到两个玄学的标记。

DFN:时间戳,表示他是第几个被访问的。

LOW:表示这个节点能向上到达的最老祖先的时间戳。

初始化时DFN=LOW

我们在搜索时把节点一个个入栈,如果在我们回溯是发现DFN==LOW,那么表示他是该子树的根,那么把该子树压出栈即可。

那大致算法框架就出来了:每次发现一个新节点就入栈,无路可走时就向后转——回溯,回溯时用子节点更新父节点的LOW值以保证他的LOW值在以他为根的子树中最小。如果发现DFN==LOW,那么就按上面所说的搞一下就行了。

还不是很懂?那我们模拟一下。

出发

首先我们从一号小朋友出发,DNF[1]=LOW[1]=++timeclock=1,入栈

然后我们欢快地继续向后,发现了二号小朋友,DFN[2]=LOW[2]=++timeclock=2,入栈

继续跑.....发现后面有一只三号小朋友,DFN[3]=LOW[3]=++timeclock=3,入栈

继续走,发现后面竟然还有一个六号小朋友,DFN[6]=LOW[6]=++timeclock=4,入栈

继续......然而六号小朋友并没有拉上其他人了,于是回头一看,DFN[6]==LOW[6],于是六号小朋友只有一个人孤独地吃果果了,出栈。

咔,找到一个SCC

继续往回走,LOW[3]=min(LOW[3],LOW[6]),然而并没有什么用,还是3。

然而我们发现DFN[3]==LOW[3],三号小朋友也拉不上人了,于是他也能独享一份果果了。

咔,又找到一个SCC

又回到二了,LOW[2]=min(LOW[2],LOW[3]),也没什么用。

但我们发现二后面还拉了一个五号小朋友,于是我们去采访一下他

五号小朋友:“DFN[5]=LOW[5]=++timeclock=5”,入栈

本来我们应该去看一下六号小朋友,但他已经在吃果果了,不在栈中,我们就不要去打扰他了。

那去一号看看吧。

一号小朋友:“我是你爸爸的爸爸.......”,管他的,既然他在栈中,那就可能在我们要继续找的SCC中,那我们还是要更一波新,表示五号小朋友能够回到的最老祖先是一号小朋友,LOW[5]=DNF[1]=1。但就没有必要去访问了,溜了溜了。

溜到二号小朋友那里,LOW[2]=min(LOW[2],LOW[5])=1。

继续溜,溜到一号小朋友那里,LOW[1]=min(LOW[1],LOW[2])=1

有点意思了,但我们不急,因为一号节点还有子节点没有访问,所以,继续走。

发现了四号小朋友,DFN=LOW=++timeclock=6

继续走,发现了五号小盆友(怎么又是你,兄弟),发现他还在栈中,更新,开溜。LOW[4]=DFN[5]=5。

回家......回到一号节点。LOW[1]=min(LOW[1],LOW[4])=1

好了,没路可走了,DFN[1]==LOW[1],一二五四,排排坐,一起吃果果。

咔,没了

没了吗?不一定,万一你没有一次并没有采访完所有小盆友呢?所以我们应该在外面套一层循环以保证所有点被遍历。

OK,大致就这样,具体实现就看代码了吧

#include<iostream>
#include<stdio.h>
using namespace std;
int n,m,cnt,p,head[100010],low[100010],dfn[100010],timeclock;
int stack[100010],top,instack[100010],belong[100010];
struct ss{
    int next,to;
};ss data[100010];
void add(int a,int b)
{
    data[++p].to=b;
    data[p].next=head[a];
    head[a]=p;
}
void dfs(int a)
{
    low[a]=dfn[a]=++timeclock;//初始化
    instack[a]=1;
    stack[++top]=a;//入栈
    for(int i=head[a];i;i=data[i].next)
    {
        if(!dfn[data[i].to])
        {
            dfs(data[i].to);
            low[a]=min(low[a],low[data[i].to]);//用子节点更新自己的LOW
        }
        else
        if(instack[data[i].to])
        low[a]=min(low[a],dfn[data[i].to]);//若是在栈中 ,比较父子关系,并更新。
    }
    if(dfn[a]==low[a])
    {
        cnt++;
        while(stack[top+1]!=a)
        {
            belong[stack[top]]=cnt;
            instack[stack[top--]]=0;
        }
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    int a,b;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&a,&b);
        add(a,b);
    }
    for(int i=1;i<=n;i++)
    {
        if(!dfn[i])
        {
           dfs(i);
        }
    }
    printf("%d ",cnt);
    for(int i=1;i<=n;i++)
    printf("%d ",belong[i]);
    return 0;
}

终于打完了,好累,但为了不用二队的刷新桌面的电脑,fighting!

附赠一个割点代码

外加定义:在一个无向图中,如果删掉点 x 后图的连通块数量增加,则称点 x 为图的割点。

外加图示

如果u点的子节点为v,v点他能返回的最老祖先比u点年轻或一样(即dfn[u]值<=low[v]),那么如果删去u点,那么v以下的点就会与v以上的点失去联系,就会产生新的连通块

也就是说如果在我们的搜索树上有一个点只有树边与祖先相连,而没有反向边连回祖先节点的话,那么它就是割点。就是没有这样的边

至于桥的道理也是差不多的

割点割点.....

#include<iostream>
#include<stdio.h>
using namespace std;
struct ss{
    int next,to;
};ss data[200010];
int n,m,p,head[100010],ge[100010];
int timeclock,ans;
int low[100010],dfn[100010];
void add(int a,int b)
{
    data[++p].to=b;
    data[p].next=head[a];
    head[a]=p;
}
void tarjan(int a,int fa)
{
    int zj=0;
    dfn[a]=low[a]=++timeclock;
    for(int i=head[a];i;i=data[i].next)
    {
        int v=data[i].to;
        if(!dfn[v])
        {
            tarjan(v,fa);
            low[a]=min(low[a],low[v]);
            if(low[v]>=dfn[a]&&a!=fa) ge[a]=1;
            if(a==fa) zj++;
        }
        low[a]=min(low[a],dfn[v]);
    }
    if(a==fa&&zj>=2) ge[a]=1;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int u,v;
        scanf("%d%d",&u,&v);
        add(u,v);add(v,u);
    }
    for(int i=1;i<=n;i++)
    if(!dfn[i])
    tarjan(i,i);
    for(int i=1;i<=n;i++)
    if(ge[i]) ans++;
    printf("%d\n",ans);
    for(int i=1;i<=n;i++)
    if(ge[i]) printf("%d ",i);
    return 0;
}

附赠一个桥的代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int N=2010;
int head[2010],num;
struct edge{
    int last,v;
};
struct data{
    int x,y;
};
data ss[400010];
edge e[400010];
void add(int from,int to)
{
    e[++num].last=head[from];
    e[num].v=to;
    head[from]=num;
}
int flag[2010],dfn[2010],low[2010];
int bu[2010],bv[2010];
int tim=0,tot;
int cnt=0;
int cmp(const data & a,const data & b)
{
    if(a.x<b.x)return 1;
    if(a.x>b.x)return 0;
    if(a.y<b.y)return 1;
    return 0;
}
void dfs(int u,int fa)
{
    tim++;
    low[u]=dfn[u]=tim;
    for(int i=head[u];i;i=e[i].last)
    {
        int v=e[i].v;
        if(!dfn[v])
        {
            dfs(v,u);
            low[u]=min(low[u],low[v]);
            if(low[v]>dfn[u])
            {
                ss[++cnt].x=u;
                ss[cnt].y=v;
            }   
        }
        else if(v!=fa)
        {
            low[u]=min(low[u],dfn[v]);
        }
    }
    return;
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    int x,y;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&x,&y);
            add(x,y);
            add(y,x);     
    }
    cnt=0;
    dfs(1,1);
    sort(ss+1,ss+cnt+1,cmp);
    printf("%d\n",cnt);
    for(int i=1;i<=cnt;i++)
    {
            printf("%d %d\n",ss[i].x,ss[i].y);
    }
    return 0;
}