zoukankan      html  css  js  c++  java
  • 浅说——tarjan

    Tarjan陪伴强连通分量,
    生成树完成后思路才闪光。
    Euler跑过的七桥古塘
    让你,心驰神往……

    图论基础知识

    相关连接,感谢以下链接(提供部分内容):

    https://big-news.cn/2019/02/07/%E5%BC%BA%E8%BF%9E%E9%80%9A%E5%88%86%E9%87%8F/

    https://www.langlangago.xyz/index.php/archives/83/

    割顶

    (特别感谢洛谷,虽然有点小错)

    Tarjan是干啥的?

    以上摘自百科

    你也看不懂,对吧。那看个毛啊!

    tarjan说白了三个用:

    1.求强连通分量

    2.求LCA

    3.无向图中,求割点和桥

    ----------------------------------------------------------------------------------------------------------------------

     强连通分量

    1.啥是强连通分量?

    百科

    这么说吧,强连通是指这个图中每两点都能够互相到达。

    强连通分量也就是最大的强连通子图,很easy吧?

    如图,有三个强连通分量(1,2,3)&(4)&(5)。

    2.怎么求强连通分量?

    噫......很明显,通过肉眼可以很直观地看出(1,2,3)是一组强连通分量,但很遗憾,代码并没有眼睛,所以该怎么判断强连通分量呢?

    如果仍是上面那张图,我们对它进行dfs遍历。

    可以注意到红边非常特别,因为如果按照遍历时间来分类的话,其他边都指向在自己之后被遍历到的点,而红边指向的则是比自己先被遍历到的点。

    如果存在这么一条边,那么我们可以yy一下

    从一个点出发,一直向下遍历,然后忽得找到一个点,那个点竟然有条指回这一个点的边!

    那么想必这个点能够从自身出发再回到自身

    想必这个点和其他向下遍历的该路径上的所有点构成了一个环,

    想必这个环上的所有点都是强联通的。

    但只是强联通啊,我们需要求的可是强连通分量啊......

    比如说图中红色强连通分量,而蓝色只是强联通图

    因此我们只需要知道这个点u下面的所有子节点有没有连着这个点的祖先就行了。

    但似乎还有一个问题啊......

    我们怎么知道这个点u它下面的所有子节点一定是都与他强联通的呢?

    这似乎是不对的,这个点u之下的所有点不一定都强联通

    那么怎么在退回到这个点的时候,知道所有和这个点u构成强连通分量的点呢?

    开个栈记录就行了

    什么?!这么简单?

    没错~就是这么简单~

    如果在这个点之后被遍历到的点已经能与其下面的一部分点(也可能就只有他一个点)已经构成强连通分量,即它已经是最大的。

    那么把它们一起从栈里弹出来就行了。

    所以最后处理到点u时如果u的子孙没有指向其祖先的边,那么它之后的点肯定都已经处理好了,一个常见的思想,可以理解一下。

    所以就可以保证栈里留下来u后的点都是能与它构成强连通分量的。

    似乎做法已经明了了,用程序应该怎么实现呢?

    3.怎么码代码?

    首先介绍一下辅助数组

    (1)、dfn[ ],表示这个点在dfs时是第几个被搜到的。
    (2)、low[ ],表示这个点以及其子孙节点连的所有点中dfn最小的值
    (3)、stack[ ],表示当前所有可能能构成强连通分量的点。
    (4)、vis[ ],表示一个点是否在stack[ ]数组中。

    那么按照之上的思路,我们来考虑这几个数组的用处以及算法的具体过程。

    假设现在开始遍历点u:

    • 首先初始化dfn[u]=low[u]=第几个被dfs到
      dfn可以理解,但为什么low也要这么做呢?
      因为low的定义如上,也就是说如果没有子孙与u的祖先相连的话,dfn[u]一定是它和它的所有子孙中dfn最小的(因为它的所有子孙一定比他后搜到)。
    • 将u存入stack[ ]中,并将vis[u]设为true
      stack[ ]有什么用?
      如果u在stack中,u之后的所有点在u被回溯到时u和栈中所有在它之后的点都构成强连通分量。(也就是上文中所说的开个栈记录)
    • 遍历u的每一个能到的点,如果这个点dfn[ ]为0,即仍未访问过,那么就对点v进行dfs,然后low[u]=min{low[u],low[v]} low[ ]有什么用?
      应该能看出来吧,就是记录一个点它最大能连通到哪个祖先节点(当然包括自己)
      如果遍历到的这个点已经被遍历到了,那么看它当前有没有在stack[ ]里,如果有那么low[u]=min{low[u],low[v]}
      如果已经被弹掉了,说明无论如何这个点也不能与u构成强连通分量,因为它不能到达u
      如果还在栈里,说明这个点肯定能到达u,同样u能到达他,他俩强联通。
    • 假设我们已经dfs完了u的所有的子树,那么之后无论我们再怎么dfs,u点的low值已经不会再变了。 
      那么如果dfn[u]=low[u]这说明了什么呢?
      再结合一下dfn和low的定义来看看吧
      dfn表示u点被dfs到的时间,low表示u和u所有的子树所能到达的点中dfn最小的。
      这说明了u点及u点之下的所有子节点没有边是指向u的祖先的了,即我们之前说的u点与它的子孙节点构成了一个最大的强连通图即强连通分量
      此时我们得到了一个强连通分量,把所有的u点以后压入栈中的点和u点一并弹出,将它们的vis[ ]置为false,如有需要也可以给它们染上相同颜色(后面会用到)

    于是tarjan求强连通分量的部分到此结束

    代码大概是这样的

    void tarjan(int u)
    {
        s.push(u);
        dfn[u]=low[u]=++k;   //dfn[u]表示u是第几个(++k)被搜到的(时间戳),low[u]表示u的连接点中最先被搜到的点v,点v是第几个被搜到的就是low[u]的值 (时间戳) 
        for(int i=head[u];i;i=e[i].next) 
        {
            int v=e[i].v;
            if(!dfn[v])  //没有搜过v,就去搜v
            {
                tarjan(v);
                low[u]=min(low[u],low[v]);
            }
            else if(!vis[v]) //搜过v,但是还在栈里 
            {
                low[u]=min(low[u],dfn[v]);
            }
        }
        int v=-1;
        if(dfn[u]==low[u])
        {
            sum++;
            while(u!=v)  //u==v就跳出了呗 
            {
                num[sum]++;
                v=s.top();
                vis[v]=1;
                s.pop();
            }
        }
    }

     来道题练练手P2863 [USACO06JAN]牛的舞会The Cow Prom

    题意为:给定一个图,要求图中节点数大于一的强联通分量个数。(模板题)

    #include<cstdio> 
    #include<string>
    #include<stack>
    using namespace std;
    int read()
    {
        int x=0,f=1;char c=getchar();
        while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
        while(isdigit(c)){x=x*10+c-'0';c=getchar();}
        return x*f;
    }
    const int maxn=10005,maxm=50005;
    int n,m;
    struct edge{
        int v,next;
    }e[maxm*2];
    int cnt,head[maxn],k;
    int dfn[maxn],low[maxn];
    bool vis[maxn];
    stack <int> s;
    int sum,num[maxn];
    int ans;
    void add(int u,int v) //建树大家都知道
    {
        e[++cnt].v=v;
        e[cnt].next=head[u];
        head[u]=cnt;
    }
    void readdata()  //呵呵快读,_|^^|●
    {
        n=read(),m=read();
        for(int i=1;i<=m;i++)
        {
            int a,b;
            a=read(),b=read();
            add(a,b);
        }
    }
    void tarjan(int u) //tar……jan
    {
         s.push(u);
        dfn[u]=low[u]=++k;   //dfn[u]表示u是第几个(++k)被搜到的(时间戳),low[u]表示u的连接点中最先被搜到的点v,点v是第几个被搜到的就是low[u]的值 (时间戳) 
        for(int i=head[u];i;i=e[i].next) 
        {
            int v=e[i].v;
            if(!dfn[v])  //没有搜过v,就去搜v
            {
                tarjan(v);
                low[u]=min(low[u],low[v]);
            }
            else if(!vis[v]) //搜过v,但是还在栈里 
            {
                low[u]=min(low[u],dfn[v]);
            }
        }
        int v=-1;
        if(dfn[u]==low[u])
        {
            sum++;
            while(u!=v)  //u==v就跳出了呗 
            {
                num[sum]++;
                v=s.top();
                vis[v]=1;
                s.pop();
            }
        }
    }
    void out()
    {
        printf("%d",ans);
    }
    void work()
    {
        for(int i=1;i<=n;i++)
        {
            if(!dfn[i])
            tarjan(i);
        }
        for(int i=1;i<=sum;i++) //大于1的强连通分量
        {
            if(num[i]>1)
            {
                ans++;
            }
        }
        out(); //shuchu,输出
    }
    int main()
    {
        readdata();
        work();
        return 0;
    }

    缩点

    1.什么时候要用缩点

    众所周知,有向无环图总是有着一些蜜汁优越性,因为没有环,你可以放心的在上面跑dfs,搞DP,但如果是一张有向有环图,事情就会变得尴尬起来了

    思考一下会发现如果不打vis标记就会t飞(一直在环里绕啊绕),但是如果打了,又不一定能保证最优解

    而你一看题目却发现显然根据一些贪心的原则,这个环上每个点的最大贡献都是整个环的总贡献

    这个时候缩点就显得很有必要了,因为单个点的贡献和整个环相同,为什么不去把整个环缩成一个超级点呢?

    这个环只是为了好理解,事实上他应该是一个强连通分量,显然如果只缩掉一个强连通图,图中仍然有环存在

    缩点的一个栗子

    2.怎么缩点

    一般用染色法(就是把强连通分量的所有点归为一组)

    来道题练练手P2341 [HAOI2006]受欢迎的牛

    所以,我们再来分析一下这道题。

    首先,不难发现,如果这所有的牛都存在同一个强联通分量里。那么它们一定互相受欢迎。

    那么,我们怎么来找明星呢。

    很简单,找出度为0的强联通分量中的点。这样可以保证所有的人都喜欢它,但是它不喜欢任何人,所以说不存在还有人事明星。

    此题还有一个特殊情况:

    如果有两个点分别满足出度为零的条件,则没有明星,这样无法满足所有的牛喜欢他。

    有了上边的解释,题目就不是那么难了,代码如下

    #include<cstdio> 
    #include<string>
    #include<iostream>
    #include<stack>
    using namespace std;
    const int maxn=10005,maxm=50005;
    int n,m;
    struct edge{
        int v,next;
    }e[maxm];
    int head[maxn],cnt;
    int dfn[maxn],low[maxn],vis[maxn],index,sum;
    int belong[maxn];
    int k,temp[maxn],forever[maxn];
    int ans[maxn];
    int cord[maxn];
    int t,key;
    stack <int> q;
    
    int read();
    void readdata();
    int add(int u,int v);
    void work();
    int tarjan(int u);
    int read();
    int find(int he);
    void out();
    
    int main()
    {
        readdata();
        work();
        out();
        return 0;
    }
    
    int read() //快读 
    {
        int x=0,f=1;char c=getchar();
        while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
        while(isdigit(c)){x=x*10+c-'0';c=getchar();}
        return x*f;
    }
    
    void out()  //输出 
    {
        for(int i=1;i<=sum;i++)
        {
            //printf("%d
    ",forever[i]);
            if(!cord[i])
            {
                t++;
                key=forever[i];
                if(t==2)  //出度数超过一个,则没有明星 
                {
                    printf("0");
                    return;
                }
            }
        }
        printf("%d",key); //输出明星个数 
    }
    
    void readdata()  //读入 
    {
        n=read(),m=read();
        for(int i=1;i<=m;i++)
        {
            int a,b;
            a=read(),b=read();
            add(a,b);   
        }
    }
    
    int add(int u,int v)   //建树 
    {
        e[++cnt].v=v;
        e[cnt].next=head[u];
        head[u]=cnt;
    }
    
    void work()  //tarjan预备操作 
    {
        for(int i=1;i<=n;i++)
        {
            if(!dfn[i])
            {
                tarjan(i);
            }
        }
    }
    
    int tarjan(int u) //tarjan 
    {
        q.push(u);
        dfn[u]=low[u]=++index;
        vis[u]=1; 
        for(int i=head[u];i;i=e[i].next)
        {
            int v=e[i].v;
            if(!dfn[v])
            {
                tarjan(v);
                low[u]=min(low[u],low[v]);
            }
            else if(vis[v])
            {
                low[u]=min(low[u],dfn[v]);
            }
        }
        int v=0; 
        if(dfn[u]==low[u])
        {
            ++sum;
            while(u!=v)
            {
                v=q.top();
                q.pop();
                belong[v]=sum;
                vis[v]=0;
                forever[sum]++;  //记录强连通分量中的结点个数 
                temp[forever[sum]]=v;  //保存该点 
            }
            cord[sum]=find(sum); //出度数 
        }
    }
    
    int find(int he) //搜索每个强连通分量的出度数 
    {
        int ans=0;
        for(int i=1;i<=forever[he];i++) 
        {
            for(int j=head[temp[i]];j;j=e[j].next) //枚举强连通分量中的每个点的出度数 
            {
                if(belong[e[j].v]!=he)
                {
                    ans++;
                }
            }
        }
        return ans;
    }

    缩点练习2P4742 [Wind Festival]Running In The Sky

    自己看题吧……QAQ

    思路:tarjan+重新建图+DAGdp

    拓扑排序没学过看看吧

    #include<cstdio> 
    #include<string>
    #include<stack>
    #include<iostream>
    #include<queue>
    using namespace std;
    
    const int maxn=200005,maxm=500005;
    int n,m;
    int k[maxn];
    struct edge{
        int v,next;
    }e[maxm];
    struct edge2{
        int v,next;
    }edag[maxm];
    int head[maxn],cnt;
    int dfn[maxn],low[maxn],vis[maxn];
    int index;
    int sum;
    int maxk[maxn],sumk[maxn],belong[maxn],forever[maxn],temp[maxn];
    int degin[maxn];
    stack <int> s;
    int head2[maxn];
    int f[maxn][2];
    queue <int> q;
    
    void readdata();                //读入 
    int read();                     //快读 
    void work();                    //计算&调用程序 
    void addedge(int u,int v);      //建树 
    void tarjan(int u);             //tarjan缩点 
    void findin();                  //dp的预处理 
    void adddag(int u,int v);       //缩点后新建树 
    void topsort();                 //拓扑排序dp 
    void out();                     //输出 
    
    int main()    //主函数 
    {
        readdata();
        work();
        topsort();
        out();
        return 0;
    }
    
    int read()    
    {
        int x=0,f=1;char c=getchar();
        while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
        while(isdigit(c)){x=x*10+c-'0';c=getchar();}
        return x*f;
    }
    
    void readdata()
    {
        n=read(),m=read();
        for(int i=1;i<=n;i++)
            k[i]=read();
        for(int i=1;i<=m;i++)
        {
            int a,b;
            a=read(),b=read();
            addedge(a,b);
        }
        cnt=0;       //比较懒,后面要用,懒得申明新的变量,直接刷0 QAQ 
    }
    
    void addedge(int u,int v)
    {
        e[++cnt].v=v;
        e[cnt].next=head[u];
        head[u]=cnt;
    }
    
    void work()
    {
        for(int i=1;i<=n;i++)  //tarjan正常操作 
        {
            if(!dfn[i])
            tarjan(i);
        }
        findin();
    }
    
    void tarjan(int u)
    {
        s.push(u);
        dfn[u]=low[u]=++index;
        vis[u]=1;
        for(int i=head[u];i;i=e[i].next)
        {
            int v=e[i].v;
            if(!dfn[v])
            {
                tarjan(v);
                low[u]=min(low[u],low[v]);
            }
            else if(vis[v])
            {
                low[u]=min(low[u],dfn[v]);
            }
        }
        if(dfn[u]==low[u])
        {
            ++sum;
            int v=0;
            while(u!=v)
            {
                v=s.top();
                s.pop(); 
                vis[v]=0;
                sumk[sum]+=k[v];                  //缩点后该点的总值 
                maxk[sum]=max(maxk[sum],k[v]);    //缩点后该点的最大值 
                belong[v]=sum;                    //记录该点的颜色 
                temp[++forever[sum]]=v;           //temp记录该颜色的一些点 
            }
        }
    }
    
    void findin()
    {
        for(int i=1;i<=n;i++)
        {
            for(int j=head[i];j;j=e[j].next)
            {
                if(belong[e[j].v]!=belong[i])    //不在同一个强连通分量中 
                {
                    adddag(belong[i],belong[e[j].v]);  //新建边 
                    degin[belong[e[j].v]]++;          //改颜色入读数++ 
                }
            }
        }
    }
    
    void adddag(int u,int v)
    {
        edag[++cnt].v=v;
        edag[cnt].next=head2[u];
        head2[u]=cnt;
    }
    
    void topsort()  //DAGdp 
    {
        for(int i=1;i<=sum;i++)  //先把入读为零的加了 
        {
            if(!degin[i])
            {
                q.push(i);
            }
        }
        while(!q.empty())
        {
            int u=q.front();
            q.pop();
            f[u][0]+=sumk[u];              //f[u][0]是到该点的最大总值 
            f[u][1]=max(f[u][1],maxk[u]);  //f[u][1]是到该点的最大总值中的最大值 
            for(int i=head2[u];i;i=edag[i].next)
            {
                int v=edag[i].v;
                degin[v]--;
                if(!degin[v])
                {
                    q.push(v);
                }
                if(f[u][0]>f[v][0])   //自己想吧 
                {
                    f[v][0]=f[u][0]; //至于这里为什么不把当前值算上,因为这里加的不一定是最大的,要没有了入度就会在while里会算上。 
                    f[v][1]=f[u][1];
                }
                else if(f[u][0]==f[v][0]) 
                {
                    f[v][1]=max(f[v][1],f[u][1]);
                }
            }
        }
    }
    
    void out()
    {
        int ans=-1,maxx=-1;
        for(int i=1;i<=sum;i++)
        {
            if(f[i][0]>ans) //同理 
            {
                ans=f[i][0];
                maxx=f[i][1];
            }
            else if(f[i][0]==ans)
            {
                maxx=max(f[i][1],maxx);
            }
        }
        printf("%d %d",ans,maxx);
    }

    割顶

    百科

    也就是:割顶是去掉以后让图不连通的点。

    思路

    首先选定一个根节点,从该根节点开始遍历整个图(使用DFS)。

    对于根节点,判断是不是割点很简单——计算其子树数量,如果有2棵即以上的子树,就是割点。因为如果去掉这个点,这两棵子树就不能互相到达。

    通过非父子边(回边),能够回溯到的最早的点(dfn最小)的dfn值(但不能通过连接u与其父节点的边)。对于边(u, v),如果low[v]>=dfn[u],此时u就是割点。

    OK?

    P3388 【模板】割点(割顶)

    #include<cstdio> 
    #include<string>
    #include<iostream>
    using namespace std;
    
    const int maxn=20005,maxm=100005;
    int n,m;
    struct edge{
        int v,next;
    }e[maxm*2];
    int head[maxn],cnt;
    int dfn[maxn],low[maxn],index;
    bool map[maxn];
    int ans;
    
    int read();
    void readdata();
    void add(int u,int v);
    void work();
    void tarjan(int u,int fa);
    void out();
    
    int main()
    {
        readdata();
        work();
        out();
        return 0;
    }
    
    int read()
    {
        int x=0,f=1;char c=getchar();
        while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
        while(isdigit(c)){x=x*10+c-'0';c=getchar();}
        return x*f;
    }
    
    void readdata()
    {
        n=read(),m=read();
        for(int i=1;i<=m;i++)
        {
            int a,b;
            a=read(),b=read();
            add(a,b);
            add(b,a);
        }
    }
    
    void add(int u,int v)
    {
        e[++cnt].v=v;
        e[cnt].next=head[u];
        head[u]=cnt;
    }
    
    void work()
    {
        for(int i=1;i<=n;i++)
        {
            if(!dfn[i])
            {
                tarjan(i,i);
            }
        }
    }
    
    void tarjan(int u,int fa)
    {
        dfn[u]=low[u]=++index;
        int child=0;
        for(int i=head[u];i;i=e[i].next)
        {
            int v=e[i].v;
            if(!dfn[v])
            {
                tarjan(v,fa);
                low[u]=min(low[u],low[v]);
                if(u!=fa&&low[v]>=dfn[u])
                    map[u]=1;
                if(u==fa)
                {
                    child++; 
                }
            }
            low[u]=min(low[u],dfn[v]);
        }
        if(u==fa&&child>=2)
        map[u]=1;
    }
    
    void out()
    {
        for(int i=1;i<=n;i++)
        {
            if(map[i])
            ans++;
        }
        printf("%d
    ",ans);
        for(int i=1;i<=n;i++)
        {
            if(map[i])
            printf("%d ",i);
        }
    }
  • 相关阅读:
    区块链初学者指南——五岁孩子都能理解
    推荐返利功能已上线——推荐好友下单,最高返45%
    干货|浅谈iOS端短视频SDK技术实现
    从单个系统到云翼一体化支撑,京东云DevOps推进中的一波三折
    体验京东云 Serverless+AI 人脸属性识别
    沙龙报名 | 云时代的架构演进—企业上云及云原生技术落地实践
    2020年9大顶级Java框架
    字符串函数
    linux添加用户
    unix/linux下线程私有数据实现原理及使用方法
  • 原文地址:https://www.cnblogs.com/mzyczly/p/11300163.html
Copyright © 2011-2022 走看看