zoukankan      html  css  js  c++  java
  • Tarjan算法整理

    众所周知,tarjan是个非常nb的人,他发明了很多nb的算法,tarjan算法就是其中一个,它常用于求解强连通分量,割点和桥等。虽然具体实现的细节不太一样,但是大体思路是差不多的。先来说一下大体思路。


     强连通分量,缩点

    我们先来定义几个东西

    时间戳:在搜索树中被遍历到的次序

    比如在下图中

     

    每个节点按照遍历顺序编的号就是它的时间戳

    dfn[i]:表示第i个点的时间戳

    low[i]:表示点i及i的子树所能追溯到的最早的节点的时间戳

    low数组看起来很难理解是不是?

    先来看一张非常经典的图

    我们发现对于结点1,3,2,4,它们的low值都是1。为什么呢?因为这些点都直接或者间接的能够追溯到的最早的点1,而点1的dfn值为1,所以这些点的low值自然也就是1了

    我们可以通过手算发现图中有三个强连通分量:{1,2,3,4},{5},{6}

    我们发现,每一个连通分量都有一个点(以下称为代表点)的low值=dfn值,也就是说这个点及它的子树所能到达的最早的点就是他自己。

    于是可以知道,对于dfn=low的点就是这一个强连通分量的代表点

    那么要求强连通分量,实际上就是求有多少个点的low=dfn

    用一个栈来实现,寻找low时只在栈里面找,弹出时不断从栈顶弹出直到弹出这个点

    代码:

    int dfn[50005],low[50005];
    //dfn表示时间戳
    //low表示点i及i的子树所能追溯到的最早的节点的时间戳
    int ind;
    //ind表示遍历顺序 
    int in[50005],s[50005],top;
    //in表示当前这个点是否在队列中
    //s是模拟的栈
    //top是栈顶 
    int cnt_scc;
    //强连通分量的个数 
    int scc[50005],cntscc[50005];
    //scc表示每一个点属于哪一个强连通分量
    //cntscc表示强连通分量的大小 
    
    void tarjan(int x)
    {
        dfn[x]=++ind;
        low[x]=dfn[x];//初始化 
        s[top++]=x;//入栈 
        in[x]=1;
        for(int i=head[x];i;i=edg[i].nxt)
        {
            int v=edg[i].to;
            if(!dfn[v])
            //如果是没有遍历到的树边就先对它进行操作 
            {
                tarjan(v);
                low[x]=min(low[x],low[v]);//更新low值 
            }
            else
            {
                if(in[v])//如果遍历过并且在栈中 
                //为什么一定要在栈中?
                //因为如果不在栈中说明它已经属于其他强连通分量了 
                //而每一次出栈都会弹出完整的强连通分量,所以这个点肯定不会产生影响 
                {
                    low[x]=min(low[x],dfn[v]);
                }
            }
        }
        if(dfn[x]==low[x])//如果找到强连通分量的代表点 
        {
            cnt_scc++;
            while(s[top]!=x)//出栈 
            {
                top--;
                in[s[top]]=0;
                scc[s[top]]=cnt_scc;
                cntscc[cnt_scc]++;
            }
        }
    }

    来看几道例题:

    P2341 [HAOI2006]受欢迎的牛

    如果有环,意味着这个环里的牛都互相喜欢

    我们可以先求出环,然后把每一个环都看作一个点,这样整个图就变成了一个DAG(有向无环图)

    看有几个点出度为0,如果大于一个点没有出边,就说明没有最受欢迎的牛,因为必定有一对牛相互不服

    如果只有一个,那么强联通分量的大小就是答案

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    
    int n,m;
    
    int cnt,head[50000];
    
    struct edge
    {
        int to,nxt;
    }edg[50005];
    
    inline void add(int from,int to)
    {
        edg[++cnt].to=to;
        edg[cnt].nxt=head[from];
        head[from]=cnt;
    }
    
    int dfn[50005],low[50005],ind,in[50005];
    int s[50005],top;
    int cnt_scc;
    int scc[50005],cntscc[50005];
    
    void tarjan(int x)
    {
        dfn[x]=++ind;
        low[x]=dfn[x];
        s[top++]=x;
        in[x]=1;
        for(int i=head[x];i;i=edg[i].nxt)
        {
            int v=edg[i].to;
            if(!dfn[v])
            {
                tarjan(v);
                low[x]=min(low[x],low[v]);
            }
            else
            {
                if(in[v])
                {
                    low[x]=min(low[x],dfn[v]);
                }
            }
        }
        if(dfn[x]==low[x])
        {
            cnt_scc++;
            while(s[top]!=x)
            {
                top--;
                in[s[top]]=0;
                scc[s[top]]=cnt_scc;
                cntscc[cnt_scc]++;
            }
        }
    }
    
    int out[50005];
    int ans;
    
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=1,x,y;i<=m;i++)
        {
            scanf("%d%d",&x,&y);
            add(x,y);
        }
        for(int i=1;i<=n;i++)
        {
            if(!dfn[i]) tarjan(i);
        }
        for(int i=1;i<=n;i++)
        {
            for(int j=head[i];j;j=edg[j].nxt)
            {
                int k=edg[j].to;
                if(scc[i]!=scc[k]) out[scc[i]]++;
            }
        }
        for(int i=1;i<=cnt_scc;i++)
        {
            if(!out[i]) 
            {
                if(!ans)
                    ans=i;
                else
                {
                    cout<<0;
                    return 0;
                }
            }
        }
        cout<<cntscc[ans];
    }

    P2002 消息传递

    我们发现如果这个题有环,那么不论在这个环上哪一个点开始传递信息,这个环中其他的点都可以到达,那么可以用tarjan把环缩成点。为了使每一个点都能被传递到,只需要找到所有入度为0的点,在这些点上开始传递信息就好了

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    
    int n,m;
    
    int head[100005],cnt;
    struct edge
    {
        int to,nxt;
    }edg[500005];
    
    inline void add(int from,int to)
    {
        edg[++cnt].to=to;
        edg[cnt].nxt=head[from];
        head[from]=cnt;
    } 
    
    int low[100005],dfn[100005],ind;
    int s[100005],top;
    bool in[100005];
    int scc[100005],cnt_scc;
    
    
    inline void tarjan(int x)
    {
        dfn[x]=++ind;
        low[x]=dfn[x];
        in[x]=1;
        s[top++]=x;
        for(int i=head[x];i;i=edg[i].nxt)
        {
            int v=edg[i].to;
            if(!dfn[v])
            {
                tarjan(v);
                low[x]=min(low[x],low[v]);
            }
            else
            {
                if(in[v])
                low[x]=min(low[x],dfn[v]);
            }
        }
        if(low[x]==dfn[x])
        {
            cnt_scc++;
            while(s[top]!=x)
            {
                in[s[--top]]=0;
                scc[s[top]]=cnt_scc;
            }
        }
    }
    
    int ans;
    int gin[100005];
    
    int main()
    {
        cin>>n>>m;
        for(int i=1;i<=m;i++)
        {
            int x,y;
            scanf("%d%d",&x,&y);
            if(x!=y)
            add(x,y);
        }
        for(int i=1;i<=n;i++)
        {
            if(!dfn[i]) tarjan(i);
        }
        for(int i=1;i<=n;i++)
        {
            for(int j=head[i];j;j=edg[j].nxt)
            {
                int v=edg[j].to;
                if(scc[v]!=scc[i])
                {
                    gin[scc[v]]++;
                }
            }
        }
        for(int i=1;i<=cnt_scc;i++)
        {
            if(!gin[i]) ans++;
        }
        cout<<ans;
    }

    类似的题还有洛谷1262,这里就先不说了


    tarjan求割点

    什么是割点?

    给你一张连通图,在上面找一个点,如果去掉这个点和所有连着它的边,整个图就不能保持连通,那么这个点就是割点

    比如这张图,里面的割点有1,4,5

    怎么求割点?

    首先选定一个dfs树的树根,从这个点开始遍历整张图。

    对于根节点,判断是不是割点显然只需要看他的子树的个数是不是大于等于2

    对于非根节点x,如果存在儿子节点y,使得dfn[x]<=low[y],则x一定是割点。

    显然如果x的所有儿子能够不经过x直接到达他的祖先,这个点就一定不是割点;反之,则说明去掉它一定会改变图的连通性

    代码:

    int low[20005],dfn[20005],ind,ans;
    bool cut[20005];
    
    inline void tarjan(int x,int fa)
    {
        dfn[x]=++ind;
        low[x]=dfn[x];
        int ch=0;
        for(int i=head[x];i;i=edg[i].nxt)
        {
            int v=edg[i].to;
            if(!dfn[v])
            {
                tarjan(v,fa);
                low[x]=min(low[x],low[v]);
                if(low[v]>=dfn[x]&&x!=fa) cut[x]=1;
                if(x==fa) ch++;
            }
            else
            {
                low[x]=min(low[x],dfn[v]);
            }
        }
        if(x==fa&&ch>=2) cut[fa]=1;
    }

    例:

    P3388 【模板】割点(割顶)

    代码:
    #include<bits/stdc++.h>
    using namespace std;
    
    int n,m;
    
    int head[20005],cnt;
    struct edge
    {
        int to,nxt;
    }edg[200005];
    
    inline void add(int from,int to)
    {
        edg[++cnt].to=to;
        edg[cnt].nxt=head[from];
        head[from]=cnt;
    } 
    
    int low[20005],dfn[20005],ind,ans;
    bool cut[20005];
    
    inline void tarjan(int x,int fa)
    {
        dfn[x]=++ind;
        low[x]=dfn[x];
        int ch=0;
        for(int i=head[x];i;i=edg[i].nxt)
        {
            int v=edg[i].to;
            if(!dfn[v])
            {
                tarjan(v,fa);
                low[x]=min(low[x],low[v]);
                if(low[v]>=dfn[x]&&x!=fa) cut[x]=1;
                if(x==fa) ch++;
            }
            else
            {
                low[x]=min(low[x],dfn[v]);
            }
        }
        if(x==fa&&ch>=2) cut[fa]=1;
    }
    
    int main()
    {
        cin>>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(i,i);
        }
        for(int i=1;i<=n;i++)
        {
            if(cut[i]==1) ans++;
        }
        cout<<ans<<endl;
        for(int i=1;i<=n;i++)
        {
            if(cut[i])
                printf("%d ",i);
        }
    }
     
  • 相关阅读:
    Best Time to Buy and Sell Stock
    Remove Nth Node From End of List
    Unique Paths
    Swap Nodes in Pairs
    Convert Sorted Array to Binary Search Tree
    Populating Next Right Pointers in Each Node
    Maximum Subarray
    Climbing Stairs
    Unique Binary Search Trees
    Remove Duplicates from Sorted Array
  • 原文地址:https://www.cnblogs.com/lcezych/p/11234574.html
Copyright © 2011-2022 走看看