Tarjan算法详解

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*2
using 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 100100
using 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*2
using 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; 
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值