• 史上代码最简单,讲解最清晰的双连通分量


     
     
     
    史上代码最简单,讲解最清晰的双连通分量
    (需提前学习强连通分量)
     
    双连通分量的主要内容包括割点、桥(割边)、点双和边双,分别对应 4 个 Tarjan 算法。
    所有算法的时间复杂度均为 O(n + m)。
    双连通分量用到 DFS 树的性质,所有的边分别树边和返祖边两类,大大简化了代码。
    双连通分量具有大量的性质,要能熟练掌握。
    一些定义:树枝边:DFS时经过的边(由上至下);
                     返祖边:与DFS方向相反,从某个节点指向某个祖先的边;
     
    注意:在无向图中,不能用dfn[fa]更新low[u];所以我们需要标记fa;
               但如果有重边,就可以;所以我们可以记录它的上一条边;利用成对储存的思想记录上一条边来判重;
     
    求割点:
        割点性质:
        (1)根结点如果是割点当且仅当其子节点数大于等于 2;
        (2)非根节点 u 如果是割点,当且仅当存在 u 的一个子树,子树中没有连向 u 的祖先的边(返祖边)。
        代码:
    void tarjan(int u,int fa) //当fa=0时,说明该节点是根节点;
    {
        int num=0; //用来计量子节点数;
        low[u]=dfn[u]=++cur;
        for(int i=head[u];i;i=star[i].to){ //链式前向星存图;
            int v=star[i].to;
            if(!dfn[v]){
                tarjan(v,u);
                low[u]=min(low[u],low[v]);
                if(!fa && ++num>1||fa && dfn[u]<=low[v]){  
                //1.根节点是割点,且子节点数大于等于2; 
                //2.非根节点是割点,且子节点中没有返祖边; 
                    cutpoint[u]=1; //标记该点为一个割点;
                }
            }
            else if(v!=fa){
                low[u]=min(low[u],dfn[v]);
            }
        }
    }

     求点双连通分量:

         以下 3 条等价(均可作为点双连通图的定义):
      (1)该连通图的任意两条边存在一个包含这两条边的简单环;
      (2)该连通图没有割点;
      (3)对于至少3个点的图,若任意两点有至少两条点不重复路径。

        下面两句话看不看的懂都行:

        点双连通分量构成对所有边集的一个划分。
        两个点双连通分量最多只有一个公共点,且必为割点。进一步地,所有点双与割点可抽象为一棵树结构。
    #include <bits/stdc++.h>
    using namespace std;
    struct littlestar{
        int to;
        int nxt;
    }star[200010];
    int head[200010],cnt;
    void add(int u,int v){
        star[++cnt].to=v;
        star[cnt].nxt=head[u];
        head[u]=cnt;
    } 
    int low[20010],dfn[20010],cur;
    pair<int,int> st[200010];
    int Top,num;
    vector<int> res[20010];
    void tarjan(int u,int fa)
    {
        low[u]=dfn[u]=++cur; 
        for(int i=head[u];i;i=star[i].nxt){ //链式前向星存图 
            int v=star[i].to;
            int top=Top;
            if(v!=fa && dfn[u]>dfn[v]){
                 st[++Top]=make_pair(u,v); //当这条边并不是通往父亲的边时,并且该点的子             
                                           //树中没有返祖边时,将这条边压入栈; 
            }
            if(!dfn[v]){
                tarjan(v,u);
                low[u]=min(low[u],low[v]);
                if(dfn[u]<=low[v]){
                    ++num; //num表示第几个点双区域(一个图可能存在多个点双) 
                    for(;Top>top;Top--){ //类似于强连通分量的退栈过程; 
                        int x=st[Top].first;
                        int y=st[Top].second;
                        if(res[x].empty() || res[x].back()!=num){
                            res[x].push_back(num); //由于num递增,所以res[]递增,所以res[x]的最后
                                                   //如果不是num,就代表之前不会标记过该点; 
                        }
                        if(res[y].empty() || res[y].back()!=num){
                            res[y].push_back(num); //与上面的同理; 
                        }
                    }
                }
            }
            else if(v!=fa){
                low[u]=min(low[u],dfn[v]);
            }
        }
    }

    求桥:

       桥的性质: (u; v)边在dfs 树中。不妨设u 为v 的父亲,v 的子树没有向u 或其祖先连的边。

    void tarjan(int u,int fa)
    {
        bool flag=0; //用来判断是否存在重边
        low[u]=dfn[u]=++cur;
        for(int i=head[u];i;i=star[i].nxt){
            int v=star[i].to;
            if(!dfn[v]){
                tarjan(v,u);
                low[u]=min(low[u],low[v]);
                if(dfn[v]==low[v]) //它的子节点v的子树中,没有像u或其祖先连的边(返祖边)
                {
                    bridge.push_back(i);  //桥的一个集合
                }
            }
            else if(v!=fa || flag){
                low[u]=min(low[u],dfn[v]);            
            }
            else flag=1;
        }
    }

    求边双连通分量

            以下3 条等价(均可作为边双连通图的定义):

        (1)该连通图的任意一条边存在一个包含这条边的简单环;
        (2)该连通图没有桥;
        (3)该连通图任意两点有至少两条边不重复路径。

        下面两句话看不看的懂都行:

        (1)边双连通分量构成对所有点集的一个划分。
        (2)两个边双连通分量最多只有一条边,且必为桥。进一步地,所有边双与桥可抽象为一棵树结构。

    #include <bits/stdc++.h>
    using namespace std;
    struct littlestar{
        int to;
        int nxt;
    }star[10010];
    int head[10010],cnt;
    void add(int u,int v){
        star[++cnt].to=v;
        star[cnt].nxt=head[u];
        head[u]=cnt;
    } 
    int st[5010],Top,num;
    int low[5010],dfn[5010],cur;
    int res[5010];
    int kk[150][150];
    int anss[5001];
    void tarjan(int u,int fa)
    {
        bool flag=0;
        low[u]=dfn[u]=++cur;
        st[++Top]=u;
        for(int i=head[u];i;i=star[i].nxt){
            int v=star[i].to;
            if(!dfn[v]){
                tarjan(v,u);
                low[u]=min(low[u],low[v]);
            }
            else if(v!=fa || flag){
                low[u]=min(low[u],dfn[v]);
            }
            else flag=1;
        }  //到此为止与求桥的意义差不多
        if(low[u]==dfn[u]){ //u的子树中,没有返祖边
            num++;
            int tmp;
            do{
                tmp=st[Top--]; //退栈,原来栈中的元素构成一个边双
                res[tmp]=num;
            }while(tmp!=u);
        }
    }
  • 相关阅读:
    Pycharm 创建py文件自动添加文件头注释
    Python 面向对象编程
    selenium webdriver入门
    测试工程师进阶必读书目
    Python好酷|抓包神器 mitmproxy
    TKinter图形化编程库
    Web UI自动化框架大比拼
    DTSE Tech Talk | 第10期:云会议带你入门音视频世界
    密码学系列之:使用openssl检测网站是否支持ocsp
    密码学系列之:在线证书状态协议OCSP详解
  • 原文地址:https://www.cnblogs.com/kamimxr/p/11053539.html
Copyright © 2020-2023  润新知