Tarjan算法详解

来源:互联网 发布:甄子丹功夫怎么样知乎 编辑:程序博客网 时间:2024/05/23 00:27

MZX大佬授课DAY2上午

tarjan是用来解决图的割边割点问题以及有向图的强连通分量(缩点)的问题的。

割边

割边是图论算法中一类很常见的问题:

定义

在一个连通图G中,假设有一条边e,去掉e后图G不再连通,那么e就是G的一条割边。换句话说,G是连通图,G-e不是连通图。

暴力算法

最暴力最暴力的算法就是每次都去掉一条边,然后进行dfs深度优先遍历。要进行n次dfs深度优先遍历。这显然效率是很低很低的。
这个时候一个叫Tarjan的男人站了出来。

tarjan算法求割边

tarjan算法是以dfs深度优先遍历算法为基础的。也就是说tarjan是对dfs的一个最优性剪枝。
在tarjan中,我们需要对每一个顶点维护两个值,dfn值和low值。
我们在dfs深度优先遍历的时候根据dfs遍历的顺序可以画出一颗dfs树(有n-1条边,这n-1条边称为树边,其余称为非树边)。我们dfs遍历的顺序编号就是dfn值,我们可以这样理解:dfn值就是一个时间戳,dfn[i]=1,就代表第i号结点是第1个被遍历到的。low值是某个节点通过非树边能够回溯到的dfn值最小的点。比如说low[8]=4;我们就能知道,8号点能通过非树边回溯到4号点。
这有什么用呢?
我们任意连接dfn上的一条非树边,发现有这样一个性质:总是祖孙结点相连的。这样的边称为返祖边。我们可以知道,如果一个点有返祖边与dfn值更小的点相连,那么它的父亲结点肯定不是割点(好好思考下,想通了再往下面看)。所以我们可以得出一个结论:在图中,如果存在一条树边(x,y),而且x是y的父亲,存在low[y]>dfn[x],那么(x,y)是割边。

先看一个例题

luogu炸铁路

分析

这道题目的大意就是要炸掉一条铁路,让这个交通系统变成两部分,这明显是一个裸的割边题目。我们这边拿这道题目作为一个模板题来讲解。

code

#include<bits/stdc++.h>#define maxn 200#define maxm 5200*2using namespace std;inline int read(){    int num=0;    char c;    bool flag=false;    for(;c>'9'||c<'0';c=getchar())    if(c=='-')    flag=true;    for(;c>='0'&&c<='9';num=num*10+c-48,c=getchar());    return flag?-num:num;}//快读/*这个代码风格……我已经被大佬们带掉了怎么说呢,就是使用namespace这个东西有啥好处呢,就是可以使整个代码的结构变得很清晰不信你看*/namespace graph//这部分是跟图论就有关系了{    int n,m,head[maxn],top=0;    struct Hydra_     {        int dot_order,next_location;    }a[maxm];//邻接表    void insert(int x,int y)    {        top++;        a[top].dot_order=y;        a[top].next_location=head[x];        head[x]=top;    }//在邻接表中插入边    void init()    {        n=read();        m=read();        for(int i=1;i<=m;i++)        {            int x=read();            int y=read();            insert(x,y);            insert(y,x);        }    }//读入}using namespace graph;//一定别忘了写这句namespace Tarjan//接下来是tarjan{    struct edge    {        int from,to;    }ans[maxm];//这是用来存储答案的一个边表    int dfn[maxn],low[maxn],tot=0,num;    void hy(int u,int fa)    //dfs函数,u代表当前结点,fa代表当前结点的父亲    {        dfn[u]=low[u]=++tot;        for(int i=head[u];i;i=a[i].next_location)        //遍历链表,我们需要遍历的是所以非u,fa的边        {            int v=a[i].dot_order;            if(v==fa)continue;            /*所以这里如果v就是u的父亲,            那么就是u,fa这条边了*/            if(!dfn[v])            //如果没有被访问过            {                hy(v,u);                //先访问递归                low[u]=min(low[u],low[v]);                //就用v的low值更新u的low值。            }            else//如果被访问过了            low[u]=min(low[u],dfn[v]);            //那就用v的dfn值更新u的low值            if(dfn[u]<low[v])            //这里是判断是否是割边,这是            {                ++num;                ans[num].from=min(u,v);                ans[num].to=max(u,v);                //把这条割边存进边表            }        }    }}using namespace Tarjan;bool mycmp(edge a,edge b){    return a.from<b.from||(a.from==b.from&&a.to<b.to);}//给边表排序的函数int main(){    init();    for(int i=1;i<=n;i++)        if(!dfn[i])            hy(i,0);    //这里是个坑,这是为了避免不连通的情况,坑了我很久啊    sort(ans+1,ans+1+num,mycmp);    for(int i=1;i<=num;i++)        printf("%d %d\n",ans[i].from,ans[i].to);    return 0;}

割点

割点是图论算法中另外一个非常常见的问题。

定义

类比割边的定义,割点就是,在连通图G中,去掉了点a,让这个图不再连通,那么就是割点。也就是G是连通图,G-a不是连通图。

暴力算法

这类题目的暴力算法一样,就是把所有跟这个点有关系的边全部去掉,再dfs。太慢了。

tarjan求割点

tarjan求割点的方法和求割边的方式类同,都需要维护两个值:low值和dfn值。意义也基本相同。我们仍然构造一颗dfs树,如果一个点有一个儿子能通过非树边到达其他非子孙结点,那么这个点就不是割点。我们可以说,如果存在一条边(x,y),x是y的父亲。如果low[y]<=dfn[x]那么说明x是割点。
需要一提的是,由于有向图都是单向边,所以非树边不一定是返祖边。可以连到兄弟子树的结点的边也是可以让这个结点不是割点的。最后要强调的是,只要有一个儿子结点满足上面的关系,那么这个点就是割点。因为至少有一个点被他阻断了呀。
思考一下根节点,根节点要怎么样才能是割点呢:度>=2

再看一个例题

[扭曲锣鼓 割点模板题(https://www.luogu.org/problem/show?pid=3388)

分析

裸模板题!!tarjan上。

code

#include<bits/stdc++.h>#define maxn 100100using namespace std;inline int read(){    int num=0;    char c;    bool flag=false;    for(;c>'9'||c<'0';c=getchar())    if(c=='-')    flag=true;    for(;c>='0'&&c<='9';num=num*10+c-48,c=getchar());    return flag?-num:num;}//快读/*还是那个套路。namespace*/namespace graph{    int n,m,head[maxn],top=0;    struct WE    {        int dot_order,next_location;    }a[maxn*2];//邻接表    void insert(int x,int y)    {        top++;        a[top].dot_order=y;        a[top].next_location=head[x];        head[x]=top;    }//插入    void init()    {        n=read();        m=read();        for(int i=1;i<=m;i++)        {            int x=read();            int y=read();            insert(x,y);            insert(y,x);        }    }//读入数据}using namespace graph;namespace Tarjan//tarjan算法{    int dfn[maxn],low[maxn],father[maxn];    /*    dfn、low的意义见上。father表示这个点    在哪个集合里面。为了搞多个连通块的问题。    */    bool ans[maxn];//用来标记某点是不是割点    int tot=0;    void hy(int x)//深度优先遍历    {        int degree=0;        dfn[x]=low[x]=++tot;//初始化dfn和low相等        for(int i=head[x];i;i=a[i].next_location)        //遍历邻接表        {            int y=a[i].dot_order;            if(!dfn[y])            {                father[y]=father[x];                //他俩有边相连,所以在一个连通块里面                hy(y);                //继续遍历                low[x]=min(low[x],low[y]);                //用y的low值来更新x的low值                if(low[y]>=dfn[x]&&x!=father[x])                    ans[x]=true;                //判断是否为割点(非根节点的情况)                if(x==father[x])                    degree++;                //如果它是根节点,父亲是自己,那么度+1            }            low[x]=min(low[x],dfn[y]);            // 用y的dfn值更新x的low值        }        if(x==father[x]&&degree>=2)            ans[x]=true;        //如果它是根节点,而且度数>=2,那么它是割点    }}using namespace Tarjan;int main(){    init();    for(int i=1;i<=n;i++)        father[i]=i;    //默认父亲是自己    memset(ans,false,sizeof(ans));    for(int i=1;i<=n;i++)        if(!dfn[i])hy(i);    //为了解决多个连通块的问题    int num=0;    for(int i=1;i<=n;i++)        if(ans[i])num++;    //统计割点数量    printf("%d\n",num);    for(int i=1;i<=n;i++)        if(ans[i])printf("%d ",i);    //打印割点编号    return 0;}

强连通分量和缩点

强连通分量是有向图中的一类很常见的问题。弄得我痛不欲生!!!

定义

有向图中一个连通分量如果满足这样的性质:其中任意两个点都是能够互相通过路径到达的,而且整个分量没有出度,那么这个连通分量就是强连通分量。

暴力算法

我给恩撒。这里写图片描述
我才不写暴力呢。

优秀的tarjan

tarjan求强连通分量是需要一个栈来维护一个被遍历到的序列,这要碰到dfn[I]=low[I]的就把所有的它以上进栈的全部弹出,这就是个强连通分量了。这个推导的话我推荐一个blog
大佬博客。关于强连通分量的证明

例题

luogu消息扩散

分析

不多说了
统计强连通分量个数。缩点。统计入度为0的强连通分量。

code

#include<bits/stdc++.h>#define maxn 100100#define maxm 500200*2using namespace std;inline int read(){    int num=0;    bool flag=true;    char c;    for(;c>'9'||c<'0';c=getchar())    if(c=='-')    flag=false;    for(;c>='0'&&c<='9';num=num*10+c-48,c=getchar());    return flag ? num : -num;}namespace graph{    int n,m,head[maxn],top1;    struct RNG    {        int dot_order,next_location;    }a[maxm];    bool tr[maxm];    struct edge    {        int x,y;    }e[maxm];    void insert(int x,int y)    {        top1++;        a[top1].dot_order=y;        a[top1].next_location=head[x];        head[x]=top1;    }    void init()    {        n=read();        m=read();        for(int i=1;i<=m;i++)        {            e[i].x=read();            e[i].y=read();            insert(e[i].x,e[i].y);        }    }}using namespace graph; namespace Tarjan{    int stack[maxm],dfn[maxn],low[maxn],tot;    bool flag[maxn];    int w[maxm];    int top=0,number=0;    void hy(int x)    {        dfn[x]=low[x]=++tot;        stack[++top]=x;        flag[x]=true;        for(int i=head[x];i;i=a[i].next_location)        {            int y=a[i].dot_order;            if(!dfn[y])            {                hy(y);                low[x]=min(low[x],low[y]);            }            else            if(dfn[y]<low[x]&&flag[y])                low[x]=dfn[y];        }        if(dfn[x]==low[x])        {            number++;            int r;            do            {                r=stack[top--];                flag[r]=false;                w[r]=n+number;            }while(x!=r);/*跟上面的代码不一样对的地方,如果碰到一个low和dfn相等的点,就弹出所有的栈中比它后入栈的点作为一个强连通分量*/        }    }}using namespace Tarjan;int main(){    init();    for(int i=1;i<=n;i++)    if(!dfn[i])    {        top=0;        hy(i);    }    int ans=0;    for(int i=1;i<=m;i++)    {        if(!tr[w[e[i].y]]&&w[e[i].x]!=w[e[i].y])        {            tr[w[e[i].y]]=true;            ans++;        }    }    printf("%d",number-ans);    return 0; }
原创粉丝点击