更好的阅读体验

前言

凡事都得靠自己 --bobo

  • 催隔壁 @K8He n 天了让他写 \(Tarjan\) 的学习笔记,但貌似还没有动静,所以决定自己写一个。

正文

  • 本文配套题单:14.图论-tarjan(强连通分量、割点、割边)

    前置知识

    • 熟练使用链式前向星
    • 在一张连通图中,所有的节点以及发生递归的边共同构成一棵搜索树。如果这张图不连通,则构成搜索森林。
    • 如图(从学校课件上扒下来的图)
      • \({\color{green}树边}\): \(DFS\) 时经过的点,即 \(DFS\) 搜索树上的边。
      • \({\color{yellow}返祖边}\):也叫回边,与 \(DFS\) 方向相反,从某个节点指向其某个祖先的边。
      • \({\color{red}横叉边}\): 从某个节点指向搜索树中另一子树终端某节点的边,它主要是在搜索的时候遇到了一个已经访问过的节点,但是这个节点并不是当前节点的祖先时形成的。
        • 无向图不存在横叉边。
      • \({\color{blue}前向边}\): 指向子树中节点的边,父节点指向子孙节点。
        • PS:前向边无用,因为通过树边就能直接到达这个点。
      • 返祖边与树边必定构成环,横叉边可能与树边构成环。
    • 时间戳、追溯值
      • 时间戳 \(dfn\) :用来标记某个节点在进 \(DFS\) 时被访问的时间顺序(由小到大)。
      • 追溯值 \(low\) :用来表示从当前节点 \(x\) 作为搜索树的根节点出发,能够访问到的所有节点中,时间戳 (\(dfn\))最小的值。
      • 当 \(dfn[x]==low[x]\) 时,以 \(x\) 为根的搜索子树中所有节点是一个强连通(从栈顶到 \(x\) 的所有节点)分量。
      • \(low[x]\) 的计算方法
        • 如果 \(x->y\) 是树边(没有被访问过),那么 \(low[x]=min(low[x],low[y])\) 。
        • 如果 \(x->y\) 是返祖边(访问过,且在栈中),那么 \(low[x]=min(low[x],dfn[y])\) 。
        • 如果 \(x->y\) 是横叉边(不在栈中),那么什么都不做。
          for(i=head[x];i!=0;i=e[i].next)
          {
          if(dfn[e[i].to]==0)
          {
          tarjan(e[i].to);
          low[x]=min(low[x],low[e[i].to]);
          }
          else
          {
          if(ins[e[i].to]==1)//ins[x]表示x是否在栈内
          {
          low[x]=min(low[x],dfn[e[i].to]);
          }
          }
          }
    • 时间复杂度 \(\Theta(N+M)\)
      • 运行 \(Tarjan\) 算法的过程中,每个节点都被访问了一次,且只进出了一次栈,每条边也只被访问了一次,故时间复杂度为 \(\Theta(N+M)\) 。

    Tarjan 与有向图

    强连通分量(Strongly Connected Components,SCC)

    • 强连通(strongly connected):在一个有向图 \(G\) 里,设有两个点 \(a,b\) ,由 \(a\) 有一条路可以走到 \(b\) ,由 \(b\) 又有一条路可以走到 \(a\) ,则称这两个顶点 \((a,b)\) 强连通。
    • 强连通图:如果在一个有向图 \(G\) 里,每两个点都强连通,则称 \(G\) 是一个强连通图。
    • 强连通分量:非强连通图有向图的极大强连通子图,称为强连通分量(Strongly Connected Components,SCC)。
    • 如果节点 \(x\) 是某个强连通分量在搜索树中遇到的第一个节点,那么这个强连通分量的其余节点肯定在搜索树中以 \(x\) 为根的子树中,节点 \(x\) 被称为这个强连通分量的根。
    • 例题
      • luogu B3609 [图论与代数结构 701] 强连通分量

        #include<bits/stdc++.h>
        using namespace std;
        #define ll long long
        #define endl '\n'
        struct node
        {
        int next,to;
        }e[100001];
        vector<int>scc[100001];
        stack<int>s;
        int head[100001],dfn[100001],low[100001],ins[100001],vis[100001],c[100001],cnt=0,tot=0,ans=0;
        void add(int u,int v)
        {
        cnt++;
        e[cnt].next=head[u];
        e[cnt].to=v;
        head[u]=cnt;
        }
        void tarjan(int x)
        {
        int i,k=0;
        tot++;
        dfn[x]=low[x]=tot;
        ins[x]=1;
        s.push(x);
        for(i=head[x];i!=0;i=e[i].next)
        {
        if(dfn[e[i].to]==0)
        {
        tarjan(e[i].to);
        low[x]=min(low[x],low[e[i].to]);
        }
        else
        {
        if(ins[e[i].to]==1)//说明e[i].to是祖先节点or左子树节点
        {
        low[x]=min(low[x],dfn[e[i].to]);
        }
        }
        }
        if(dfn[x]==low[x])//如果这里构成了一个强连通分量
        {
        ans++;//ans是强连通分量的编号
        while(x!=k)//将这个强连通分量内的点标记一下
        {
        k=s.top();
        ins[k]=0;
        c[k]=ans;//c[k]表示k属于哪个强连通分量内
        scc[ans].push_back(k);
        s.pop();
        }
        }
        }
        int main()
        {
        int n,m,i,j,u,v;
        cin>>n>>m;
        for(i=1;i<=m;i++)
        {
        cin>>u>>v;
        add(u,v);
        }
        for(i=1;i<=n;i++)
        {
        if(dfn[i]==0)
        {
        tarjan(i);
        }
        }
        cout<<ans<<endl;
        for(i=1;i<=n;i++)
        {
        if(vis[c[i]]==0)
        {
        vis[c[i]]=1;
        sort(scc[c[i]].begin(),scc[c[i]].end());
        for(j=0;j<scc[c[i]].size();j++)
        {
        cout<<scc[c[i]][j]<<" ";
        }
        cout<<endl;
        }
        }
        return 0;
        }
      • luogu P2661 [NOIP2015 提高组] 信息传递

        • 易知本题答案为最小的强连通分量大小(非 \(1\) )
          scc=0;
          while(x!=k)
          {
          k=s.top();
          ins[k]=0;
          scc++;
          s.pop();
          }
          if(scc!=1)
          {
          maxin=min(maxin,scc);
          }

          \(maxin\) 即为结果

      • luogu P2863 [USACO06JAN] The Cow Prom S

        • 按照题意打就行
      • luogu P2921 [USACO08DEC] Trick or Treat on the Farm G 先跑一遍 \(Tarjan\) (这是一个有 \(n\) 个点,\(n\) 条边的图,故只有 \(1\) 个强连通分量的大小不为 \(1\) ),然后分类讨论:

        • 若节点 \(x\) 所在强连通分量的大小不为 \(1\) ,则答案为 \(x\) 所在强连通分量的大小。
        • 若节点 \(x\) 所在强连通分量的大小为 \(1\) ,则答案为节点 \(x\) 到不为1的强连通分量的距离 \(+\) 这个强连通分量的大小。
          • 至于怎么求距离,记忆化搜索是个好东西。
            #include<bits/stdc++.h>
            using namespace std;
            #define ll long long
            #define sort stable_sort
            #define endl '\n'
            struct node
            {
            int next,to;
            }e[100001];
            stack<int>s;
            int head[100001],dfn[100001],low[100001],ins[100001],scc[100001],c[100001],u[100001],v[100001],anss[100001],cnt=0,tot=0,ans=0;
            void add(int u,int v)
            {
            cnt++;
            e[cnt].next=head[u];
            e[cnt].to=v;
            head[u]=cnt;
            }
            void tarjan(int x)
            {
            int i,k=0;
            tot++;
            dfn[x]=low[x]=tot;
            ins[x]=1;
            s.push(x);
            for(i=head[x];i!=0;i=e[i].next)
            {
            if(dfn[e[i].to]==0)
            {
            tarjan(e[i].to);
            low[x]=min(low[x],low[e[i].to]);
            }
            else
            {
            if(ins[e[i].to]==1)
            {
            low[x]=min(low[x],dfn[e[i].to]);
            }
            }
            }
            if(dfn[x]==low[x])
            {
            ans++;
            while(x!=k)
            {
            k=s.top();
            ins[k]=0;
            c[k]=ans;
            scc[ans]++;
            s.pop();
            }
            }
            }
            void search(int rt,int x,int sum)
            {
            if(anss[x]==0)
            {
            search(rt,v[x],sum+1);
            }
            else
            {
            anss[rt]=anss[x]+sum;
            return;
            }
            }
            int main()
            {
            int n,i,j,sum;
            cin>>n;
            for(i=1;i<=n;i++)
            {
            u[i]=i;
            cin>>v[i];
            add(u[i],v[i]);
            }
            for(i=1;i<=n;i++)
            {
            if(dfn[i]==0)
            {
            tarjan(i);
            }
            }
            for(i=1;i<=n;i++)
            {
            if(u[i]==v[i])//给自环打个标记
            {
            anss[i]=1;
            }
            else
            {
            if(scc[c[u[i]]]>=2)
            {
            anss[i]=scc[c[u[i]]];
            }
            }
            }
            for(i=1;i<=n;i++)
            {
            if(anss[i]==0)
            {
            search(u[i],v[i],1);
            }
            }
            for(i=1;i<=n;i++)
            {
            cout<<anss[i]<<endl;
            }
            return 0;
            }

    缩点

    • 缩点:把强连通分量看作一个大点,并保留不在此强连通分量内的边,重新建图(易知此时的图是一个 DAG ,可以进行拓扑排序)。
      if(dfn[x]==low[x])
      {
      ans++;
      while(x!=k)
      {
      k=s.top();
      ins[k]=0;
      c[k]=ans;
      b[ans]+=a[k];//a[]为原点权,b[]为缩点后的点权
      s.pop();
      }
      }
      cnt=0;
      memset(e,0,sizeof(e));
      memset(head,0,sizeof(head));
      for(i=1;i<=m;i++)
      {
      if(c[u[i]]!=c[v[i]])//Pursuing_OIer 教给我的神奇的建边方法(其实很好理解)
      {
      add(c[u[i]],c[v[i]]);
      }
      }
    • 例题
      • luogu P5145 漂浮的鸭子 跑一遍缩点,求最大环,把边权转换为点权即可。
      • luogu P2002 消息扩散luogu P2835 刻录光盘观察题意,易知结果为缩点后入度为 \(0\) 的点个数。
      • luogu P2812 校园网络【[USACO]Network of Schools加强版】 第一问显然同luogu P2002,令 \(p,q\) 分别表示缩点后入度、出度为 \(0\) 的点的个数,第二问显然是要求 \(max(p,q)\) , 只有一个 \(SCC\) 时记得特判。

        双倍经验 luogu P2746 [USACO5.3] 校园网Network of Schools UVA12167 Proving Equivalences
      • luogu P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G 观察题意,易知缩点后若有强连通分量的个数 \(>1\) ,则无解;当只有 \(1\) 个强连通分量时,这个强连通分量的大小即为所求。
      • luogu P2515 [HAOI2010] 软件安装 读完题,发现和luogu P2014 [CTSC1997] 选课 很像,只不过选课是一棵树,本题是一个图。可能本题建图有点麻烦,剩下的话,缩点后当树形 \(DP\) 做就行了。
        for(i=1;i<=n;i++)
        {
        cin>>u[i];
        v[i]=i;
        if(u[i]!=0)
        {
        add(u[i],v[i]);
        }
        } ······ cnt=0;
        memset(e,0,sizeof(e));
        memset(head,0,sizeof(head));
        for(i=1;i<=n;i++)
        {
        if(c[u[i]]!=c[v[i]])
        {
        add(c[u[i]],c[v[i]]);
        din[c[v[i]]]++;
        }
        }
        for(i=1;i<=ans;i++)
        {
        if(din[i]==0)
        {
        add(0,i);
        }
        }
      • luogu P3387 【模板】缩点 缩点后跑最长路( \(SPFA\) or 拓扑)。
        int top_sort()
        {
        queue<int>q;
        int i,k,num=0;
        for(i=1;i<=ans;i++)
        {
        if(din[i]==0)
        {
        q.push(i);
        dis[i]=b[i];//b[]为缩点后的点权
        }
        }
        while(q.empty()==0)
        {
        k=q.front();
        q.pop();
        for(i=head[k];i!=0;i=e[i].next)
        {
        dis[e[i].to]=max(dis[e[i].to],dis[k]+b[e[i].to]);
        din[e[i].to]--;
        if(din[e[i].to]==0)
        {
        q.push(e[i].to);
        }
        }
        }
        for(i=1;i<=ans;i++)
        {
        num=max(num,dis[i]);
        }
        return num;
        }
      • CF999E Reachability from the Capital 读题,易知对于每个强连通分量缩点后入度为 \(0\) 的点的数量即为所求(起点所在强连通分量要特判),代码
      • luogu P3119 [USACO15JAN] Grass Cownoisseur G 读题,每个点原点权均为1(且一个点的点权只能算一次),题目要求分别以 \(1\) 为起点和终点(建反图就行了)的最长路,果断 \(SPFA\) ,但是这个缩点后建边不太好搞…………
        • 因为分别以 \(1\) 为起点和终点(建反边就行了)的最长路会把 \(1\) 多算一次,记得减去 \(1\) 的点权。
        • 结果初始值要设为 \(b[c[1]]\) ,因为可能没有边与 \(1\) 相连,比如 两张图分别对应原图和反图。
        • 剩下的细节看代码吧。
      • luogu P2321 [HNOI2006] 潘多拉的宝盒
        • 简化题意:有 \(s\) 个咒语机,每个咒语机有 \(n\) 个元件,每个元件的出度为 \(2\) (字符串后面加 \(0\) 指向一个元件,字符串后面加 \(1\) 指向另一个元件),又有 \(m\) 个输出元。称从一个元件出发,以一个咒语机结尾而产生的字符串为一种方案。若咒语机 \(x\) 产生的所有方案包含咒语机 \(y\) 产生的所有方案,则称咒语机 \(x\) 是咒语机 \(y\) 的升级。求最长升级序列的长度。
        • 本题难在建图,但建图后 \(Tarjan\) 缩点+记忆化搜索记录最长链即可。
          • 若在搜索过程中,一个宝盒出现输出元,而另一个没有没有出现,此时一定有这两个宝盒间不存在包含关系,即不存在升级;否则建一条起点为i,终点为j的有向边。
        • 代码
        • 潘多拉(图片来源:奥奇传说)
      • CF131D Subway
        • 本题有些特殊,因为是无向图。
        • 题解 懒得搬了。

    Tarjan 与无向图

    无向图与割点(割顶)、割边(桥)

    • 在一个无向图中,不存在横叉边(因为边是双向的)。
    • 一个无向图中,可能不止存在一个割点或一条割边。
    • 割点(割顶):在一个无向图中,若删除节点 \(x\) 以及所有与 \(x\) 相关联的边之后,图将会被分成两个或者两个以上不相连的子图,那么称 \(x\) 为这个图的割点(割顶)。
      • 判定法则:

        当遍历到一个节点 \(x\) 时,这个点为割点的情况有两种:

        • 该节点为根节点且子节点的个数 \(>1\) (易知此时对于 \(x\) 的任意一个子节点 \(y\) 都有 \(dfn[x]<low[y]\) ),则删掉这个节点 \(x\) 后必将导致子节点不连通,即该节点 \(x\) 为图的一个割点。
        • 该节点不为根节点,且存在一个子节点 \(y\) 使得 \(dfn[x]≤low[y]\)(子节点 \(y\) 可回溯到的最早节点不早于 \(x\) 点,即子节点 \(y\) 无法回到 \(x\) 的祖先节点) ,则删掉这个节点 \(x\) 后必将导致 \(x\) 的父节点与 \(x\) 的子节点不连通,即该节点 \(x\) 为图的一个割点。
          • 若不存在一个子节点 \(y\) 使得 \(dfn[x]≤low[y]\),说明子节点 \(y\) 能绕行其他边到达比 \(x\) 更早访问的节点, \(x\) 就不是本图的割点,即环内的点割不掉。
      • 应用:如图,节点 \((0,4,5,6,7,11)\) 为割点。
      • 例题
        • luogu P3388 【模板】割点(割顶)
          #include<bits/stdc++.h>
          using namespace std;
          #define ll long long
          #define endl '\n'
          struct node
          {
          int next,to;
          }e[400001];
          int head[400001],dfn[400001],low[400001],flag[400001],cnt=0,tot=0;
          void add(int u,int v)
          {
          cnt++;
          e[cnt].next=head[u];
          e[cnt].to=v;
          head[u]=cnt;
          }
          void tarjan(int x,int fa)
          {
          int i,k=0,son=0;//son用于存子节点个数
          tot++;
          dfn[x]=low[x]=tot;
          for(i=head[x];i!=0;i=e[i].next)
          {
          if(dfn[e[i].to]==0)
          {
          tarjan(e[i].to,fa);
          low[x]=min(low[x],low[e[i].to]);
          if(low[e[i].to]>=dfn[x])
          {
          son++;
          if(x!=fa||son>=2)
          {
          flag[x]=1;
          }
          }
          }
          else
          {
          low[x]=min(low[x],dfn[e[i].to]);
          }
          }
          }
          int main()
          {
          int n,m,i,u,v,sum=0;
          cin>>n>>m;
          for(i=1;i<=m;i++)
          {
          cin>>u>>v;
          add(u,v);
          add(v,u);
          }
          for(i=1;i<=n;i++)
          {
          if(dfn[i]==0)
          {
          tarjan(i,i);
          }
          }
          for(i=1;i<=n;i++)
          {
          sum+=flag[i];
          }
          cout<<sum<<endl;
          for(i=1;i<=n;i++)
          {
          if(flag[i]==1)
          {
          cout<<i<<" ";
          }
          }
          return 0;
          }

          双倍经验(输入格式有点ex) UVA315 Network

        • luogu P3469 [POI2008] BLO-Blockade
          • 考虑分类讨论:

            • 若节点 \(x\) 不是割点,则删去所有与 \(x\) 相连的边后,只有 \(x\) 与其他 \(n-1\) 个点不连通,其他 \(n-1\) 个点之间仍然是连通的,故此时答案为 \(2(n-1)\) 。

              • PS:因为是有序点对,即 \((x,y)\) 和 \((y,x)\) 是不同的点对。
            • 若节点 \(x\) 是割点,则删去所有与 \(x\) 相连的边后,图为分成若干个连通块,求出各个连通块的大小后,两两相乘再相加即为此时答案。
              • 设在搜索树中,节点 \(x\) 的子节点集合中,有 \(t\) 个节点 \(s_{1},s_{2},s_{3},…,s_{t}\) 满足割点判定法则 \(dfn[x]≤low[s_{k}](1≤k≤t)\) 。则删去所有与 \(x\) 相连的边后,这个图至多分成 \(t+2\) 个连通块,每个连通块的节点构成情况如下:

                • \(1\) 个由节点 \(x\) 自身单独构成的连通块。
                • \(t\) 个由搜索树上以 \(s_{k}(1≤k≤t)\) 为根的子树的节点构成的连通块。
                • \(1\) 个由除了上述节点之外的所有节点构成的连通块(可以没有)。eg:搜索树上 \(x\) 父亲节点方向的所有节点。
              • 跑一遍 \(Tarjan\) 求出搜索树每个子树的大小。设 \(size[x]\) 用来表示以 \(x\) 为根的子树大小,故此时答案为 \(size[s_{1}]×(n-size[s_{1}])+size[s_{2}]×(n-size[s_{2}])+…+size[s_{t}]×(n-size[s_{t}])+1×(n-1)+(n-1- \sum\limits_{k=1}^{t} size[s_{k}] )×(1+ \sum\limits_{k=1}^{t} size[s_{k}] )\) 。
          • 剩下细节看代码吧。
          • 双倍经验 SP15577 STC10 - Blockade
    • 割边(桥):在一个无向图中,若删除边 \(e\) 之后,图将会被分成两个不相连的子图,那么称 \(e\) 为这个图的割边(桥)。
      • 判定法则:当遍历到一个节点 \(x\) 时,与其子节点 \(y\) 的这条边 \(e\) ,使得 \(dfn[x]<low[y]\) (从 \(y\) 出发,若不经过边 \(e\) ,则无法到达 \(x\) 或 更早访问的节点),则这条边 \(e\) 为图的一条割边。

        • 若 \(low[y]≤dfn[x]\) ,则说明 \(y\) 能通过其他的边到达 \(x\) 或 更早访问的节点,即这条边 \(e\) 不是图的一条割边。
      • 注意事项:
        • 有重边的边一定不是割边。
        • 边是无方向的,所以父亲与孩子之间节点的关系需要自己规定,防止误认为环。
        • 两个割点之间的边不一定是割边;割边的两个端点不一定是割点,但至少有一个是割点(eg:有重边)。
      • 例题
        • luogu P1656 炸铁路 读题,易知结果为图中的割边。
          #include<bits/stdc++.h>
          using namespace std;
          #define ll long long
          #define sort stable_sort
          #define endl '\n'
          struct node
          {
          int from,to;
          }e[2001];
          vector<int>E[2001];
          int dfn[2001],low[2001],flag[2001],cnt=0,tot=0;
          void add(int u,int v)
          {
          cnt++;
          e[cnt].from=min(u,v);
          e[cnt].to=max(u,v);
          }
          void tarjan(int x,int fa)
          {
          int i,k=0,pd=0;//pd用来记录遍历x的子节点时是否回到了fa
          tot++;
          dfn[x]=low[x]=tot;
          for(i=0;i<E[x].size();i++)//不要写成i<=E[x].size()-1
          {
          if(dfn[E[x][i]]==0)
          {
          tarjan(E[x][i],x);
          low[x]=min(low[x],low[E[x][i]]);
          if(low[E[x][i]]>dfn[x])
          {
          add(x,E[x][i]);
          }
          }
          else
          {
          if(E[x][i]==fa&&pd==0)
          {
          pd=1;
          }
          else//若pd==1,则将其当作计算儿子节点的方法来更新当前节点的值。
          {
          low[x]=min(low[x],dfn[E[x][i]]);
          }
          }
          }
          }
          bool cmp(node a,node b)
          {
          if(a.from==b.from)
          {
          return a.to<b.to;
          }
          else
          {
          return a.from<b.from;
          }
          }
          int main()
          {
          int n,m,i,u,v;
          cin>>n>>m;
          for(i=1;i<=m;i++)
          {
          cin>>u>>v;
          E[u].push_back(v);
          E[v].push_back(u);
          }
          for(i=1;i<=n;i++)
          {
          if(dfn[i]==0)
          {
          tarjan(i,i);
          }
          }
          sort(e+1,e+1+cnt,cmp);
          for(i=1;i<=cnt;i++)
          {
          cout<<e[i].from<<" "<<e[i].to<<endl;
          }
          return 0;
          }

    无向图与双连通分量

    • 若一个无向连通图不存在割点,则称它为点双连通图。
    • 若一个无向连通图不存在割边,则称它为边双连通图。
    • 无向图中极大的点双连通子图叫点双连通分量( \(v-DCC\) )。
    • 无向图中极大的边双连通子图叫边双连通分量( \(e-DCC\) )。
      • eg:
    • 点双连通分量和边双连通分量统称为双连通分量。
    • 在一张连通的无向图中,对于两个点 \(x\) 和 \(y\) ,如果删去哪条边(只能删去一条)都不能使它们不连通,我们就说 \(x\) 和 \(y\) 边双连通。
    • 在一张连通的无向图中,对于两个点 \(x\) 和 \(y\) ,如果删去哪个点(只能删去一个,且不能删去 \(x\) 和 \(y\) 自己)都不能使它们不连通,我们就说 \(x\) 和 \(y\) 点双连通。

    点双连通分量

    • 点双连通分量

      • 若某个点为孤立点,则它自己单独构成一个 \(v-DCC\) 。
      • 除了孤立点之外,点双连通分量的大小至少为 \(2\) 。
      • 性质
        • 点双连通分量之间以割点连接,且两个点双连通分量之间有且只有一个割点。

          • 证明:若两个点双连通分量之间共用两个点,则删除其中任意一个点,所有点依旧连通。如图,
        • 每一个割点可任意属于多个点双连通分量,因此求点双连通分量时,可能包含重复的点。
        • 每一个割点都在至少两个点双连通分量中。
          • 证明:在一个非点双连通图中,删去割点后图会不连通,故割点至少连接着图的两部分。但是因为点双连通图中不存在割点,所以这两部分肯定不在同一个点双连通分量中。因此割点至少存在于两个点双连通分量中。
        • 只有一条边连通的两个点也是一个点双连通分量。如图
        • 除了上一条中的情况外,其他的点双连通分量都满足任意两点间都存在不少于两条点不重复路径。
        • 任意一个不是割点的点都只存在于一个点双连通分量中。
        • 点双连通不具有传递性,如图,\((1,3)\) 点双连通,\((1,7)\) 点双连通,但是 \((3,7)\) 不点双连通。
      • 应用:如图,存在( \(1,2,3\) ) , ( \(3,4\) ) , ( \(4,5,6\) ) 这三个点双连通分量。
      • 算法

        用一个栈存点,若遍历回到 \(x\) 时,发现割点判定法则 \(dfn[x]≤low[y]\) 成立,则从栈中弹出节点,直到 \(y\) 被弹出。那么,刚才弹出的节点和 \(x\) 一起构成一个 \(v-DCC\) 。
    • 缩点
      • 对每一个割点和 \(v-DCC\) 建点,然后根据从属关系连边,构成一棵树(或森林)。
    • 例题
      • luogu P8435 【模板】点双连通分量

        • 事实上在求割点的同时,同时可以顺便求出点双连通分量,维护一个栈在求割点的途中若有 \(dfn[x]>low[y]\) ,则将 \((x,y)\) 入栈;而当 \(dfn[x]≤low[y]\) 时,将栈中所有在 \((x,y)\) 之上的边全部取出,这些边所连接的点与 \(x\) 构成了一个点双连通分量,显然割点是可以属于多个点双连通分量的。
        • 每当新搜到一个节点时,将其压入栈中。
        • 当发现 \(x\) 的子节点 \(y\) 不能通过其他方式到达 \(x\) 的祖先,但可以到达 \(x\)(即 \(dfn[x]≤low[y]\) 成立) ,则弹出栈顶元素直到 \(y\) 弹出。
        • 弹出的所有元素组成的集合 \(E\) 加上 \(x\) ,则为一个点双连通分量。
        #include<bits/stdc++.h>
        using namespace std;
        #define ll long long
        #define endl '\n'
        struct node
        {
        int next,to;
        }e[4000001];
        vector<int>v_dcc[4000001];
        stack<int>s;
        int head[4000001],dfn[4000001],low[4000001],cnt=0,tot=0,ans=0;
        void add(int u,int v)
        {
        cnt++;
        e[cnt].next=head[u];
        e[cnt].to=v;
        head[u]=cnt;
        }
        void tarjan(int x,int fa)
        {
        int i,k=0;
        if(x==fa&&head[x]==0)//孤立点判定
        {
        ans++;
        v_dcc[ans].push_back(x);
        }
        tot++;
        dfn[x]=low[x]=tot;
        s.push(x);
        for(i=head[x];i!=0;i=e[i].next)
        {
        if(dfn[e[i].to]==0)
        {
        tarjan(e[i].to,fa);
        low[x]=min(low[x],low[e[i].to]);
        if(low[e[i].to]>=dfn[x])
        {
        ans++;
        v_dcc[ans].push_back(x);
        while(e[i].to!=k)//弹栈时不能弹出割点,因为割点属于多个点双连通分量
        {
        k=s.top();
        v_dcc[ans].push_back(k);
        s.pop();
        }
        }
        }
        else
        {
        low[x]=min(low[x],dfn[e[i].to]);
        }
        }
        }
        int main()
        {
        int n,m,i,j,u,v;
        cin>>n>>m;
        for(i=1;i<=m;i++)
        {
        cin>>u>>v;
        if(u!=v)//重边会影响结果,记得特判
        {
        add(u,v);
        add(v,u);
        }
        }
        for(i=1;i<=n;i++)
        {
        if(dfn[i]==0)//注意图可能不连通
        {
        tarjan(i,i);
        }
        }
        cout<<ans<<endl;
        for(i=1;i<=ans;i++)
        {
        cout<<v_dcc[i].size()<<" ";
        for(j=0;j<v_dcc[i].size();j++)
        {
        cout<<v_dcc[i][j]<<" ";
        }
        cout<<endl;
        }
        return 0;
        }
      • luogu B3610 [图论与代数结构 801] 无向图的块
        • 此题中的块即为大小不为 \(1\) 的点双连通分量,故不需要判断孤立点了。
        • 再按字典序排序一下就行。
        #include<bits/stdc++.h>
        using namespace std;
        #define ll long long
        #define endl '\n'
        struct node
        {
        int next,to;
        }e[4000001];
        vector<int>v_dcc[4000001];
        stack<int>s;
        int head[4000001],dfn[4000001],low[4000001],cnt=0,tot=0,ans=0;
        void add(int u,int v)
        {
        cnt++;
        e[cnt].next=head[u];
        e[cnt].to=v;
        head[u]=cnt;
        }
        void tarjan(int x,int fa)
        {
        int i,k=0;
        tot++;
        dfn[x]=low[x]=tot;
        s.push(x);
        for(i=head[x];i!=0;i=e[i].next)
        {
        if(dfn[e[i].to]==0)
        {
        tarjan(e[i].to,fa);
        low[x]=min(low[x],low[e[i].to]);
        if(low[e[i].to]>=dfn[x])
        {
        ans++;
        v_dcc[ans].push_back(x);
        while(e[i].to!=k)
        {
        k=s.top();
        v_dcc[ans].push_back(k);
        s.pop();
        }
        }
        }
        else
        {
        low[x]=min(low[x],dfn[e[i].to]);
        }
        }
        }
        bool cmp(vector<int> x,vector<int> y)
        {
        for(int i=0;i<min(x.size(),y.size());i++)
        {
        if(x[i]!=y[i])
        {
        return x[i]<y[i];
        }
        }
        return x.size()<y.size();
        }
        int main()
        {
        int n,m,i,j,u,v;
        cin>>n>>m;
        for(i=1;i<=m;i++)
        {
        cin>>u>>v;
        if(u!=v)
        {
        add(u,v);
        add(v,u);
        }
        }
        for(i=1;i<=n;i++)
        {
        if(dfn[i]==0)
        {
        tarjan(i,i);
        }
        }
        cout<<ans<<endl;
        for(i=1;i<=ans;i++)
        {
        sort(v_dcc[i].begin(),v_dcc[i].end());
        }
        sort(v_dcc+1,v_dcc+1+ans,cmp);
        for(i=1;i<=ans;i++)
        {
        for(j=0;j<v_dcc[i].size();j++)
        {
        cout<<v_dcc[i][j]<<" ";
        }
        cout<<endl;
        }
        return 0;
        }
      • luogu P3225 [HNOI2012] 矿场搭建
        • 在求点双连通分量的时候把割点顺便求出来,令点双连通分量的大小为 \(size\) ,然后分类讨论:

          • 当一个点双连通分量中没有割点时,需要建两个救援出口,方案总数增加 \(C_{size}^2=\frac{size!}{{(size-2)}!×2!}=\frac{size(size-1)}{2}\) 。如图,\((1,2,3,4)\) 为本图的点双连通分量,且没有割点,则在 \((1,2,3,4)\) 中任选两个点作为救援出口。
          • 当一个点双连通分量中有 \(1\) 个割点时,需要建一个救援出口(不能建在割点上),方案总数增加 \(C_{size-1}^1=\frac{(size-1)!}{{(size-2)}!×1!}=size-1\) 。如图, \((1,2,6,3,5),(1,4)\) 为本图的两个点双连通分量,且 \(1\) 为本图的割点,则在 \((2,6,3,5),(4)\) 中各任选出一个点作为救援出口。
          • 当一个点双连通分量中的割点个数大于 \(1\) ,不需要建救援出口。如图,点双连通分量 \((2,5,6)\) 中有两个割点,则不需要建救援出口。
            • 证明:当割点坍塌后,可以通过另一个割点达到其他点双连通分量。
        • 剩下的看代码吧。
        • 三倍经验 SP16185 BUSINESS - Mining your own business UVA1108 Mining Your Own Business

    边双连通分量

    • 边双连通分量

      • 性质

        • 边双连通具有传递性,即若 \(x,y\) 边双连通,\(y,z\) 边双连通,则 \(y,z\) 边双连通。如图,\((1,3)\) 边双连通,\((1,7)\) 边双连通,则 \((3,7)\) 边双连通。
      • 算法
        • 跑一遍 \(Tarjan\) 求出所有割边,然后把割边去掉,无向图会分成若干个连通块,每个连通块就是一个边双连通分量。
      • 例题
        • luogu P8436 【模板】边双连通分量

          #include<bits/stdc++.h>
          using namespace std;
          #define ll long long
          #define endl '\n'
          struct node
          {
          int next,to,flag;
          }e[4000001];
          vector<int>e_dcc[4000001];
          stack<int>s;
          int head[4000001],dfn[4000001],low[4000001],flag[4000001],dout[4000001],cnt=1,tot=0,ans=0;
          void add(int u,int v)
          {
          cnt++;
          e[cnt].next=head[u];
          e[cnt].flag=0;
          e[cnt].to=v;
          head[u]=cnt;
          }
          void tarjan(int x,int fa)
          {
          int i,k=0;
          tot++;
          dfn[x]=low[x]=tot;
          s.push(x);
          for(i=head[x];i!=0;i=e[i].next)
          {
          if(dfn[e[i].to]==0)
          {
          tarjan(e[i].to,i);
          low[x]=min(low[x],low[e[i].to]);
          if(low[e[i].to]>dfn[x])//割边判定法则
          {
          e[i].flag=e[i^1].flag=1;
          }
          }
          else
          {
          if((fa^1)!=i)
          {
          low[x]=min(low[x],dfn[e[i].to]);
          }
          }
          }
          if(low[x]==dfn[x])//这里可以用DFS替代
          {
          ans++;
          while(x!=k)
          {
          k=s.top();
          s.pop();
          e_dcc[ans].push_back(k);
          }
          }
          }
          int main()
          {
          int n,m,i,j,u,v;
          cin>>n>>m;
          for(i=1;i<=m;i++)
          {
          cin>>u>>v;
          if(u!=v)
          {
          add(u,v);
          add(v,u);
          }
          }
          for(i=1;i<=n;i++)
          {
          if(dfn[i]==0)
          {
          tarjan(i,0);
          }
          }
          cout<<ans<<endl;
          for(i=1;i<=ans;i++)
          {
          cout<<e_dcc[i].size()<<" ";
          for(j=0;j<e_dcc[i].size();j++)
          {
          cout<<e_dcc[i][j]<<" ";
          }
          cout<<endl;
          }
          return 0;
          }
    • 缩点
      • 算法:把每一个边双连通分量缩成一个点使得原本的连通图变成一棵树(若原图不连通,则为树林),且树边就是原图的割边。其中满足 \(low[x]==dfn[x]\) 的节点 \(x\) ,这必定是一个边双连通分量的根。

        • 证明:在一个边双连通分量中没有割边,且节点 \(x\) 满足 \(low[x]==dfn[x]\) ,则在以 \(x\) 为根的子树内没有一个节点有连边到该节点的祖先节点,所以仍在栈中的节点一定是以 \(x\) 为根的边双连通分量。
      • 例题
        • luogu P2860 [USACO06JAN] Redundant Paths G

          • 简化题意:给定一个连通的无向图进行加边操作,使得每一对点之间都至少有两条相互分离的路径(两条路径没有一条重合的道路),即使得所有的节点都在环上(可能是不同的环),也可以理解为每个节点的入度至少为 \(2\) ,求最小的加边数。
          • 考虑跑一遍 \(e-DCC\) 的缩点,找出所有的叶子节点(即入度为 \(1\) 的节点),设其个数为 \(leaf\) ,则结果为 $\left \lceil \frac{leaf}{2} \right \rceil $ 。
            • 如图, \(1,3,4,5\) 为以 \(2\) 为根的树中的叶子节点( \(leaf=4\) ),则连接 \(1->3\) , \(4->5\) 为满足题意的一组解( \(\left \lceil \frac{leaf}{2} \right \rceil =2\) )。
            • 如图, \(1,3,4,5,6\) 为以 \(2\) 为根的树中的叶子节点( \(leaf=5\) ),则连接 \(1->3\) , \(4->5\) , \(3->6\) 为满足题意的一组解( \(\left \lceil \frac{leaf}{2} \right \rceil =3\) )。
          • 代码
        • CF652E Pursuit For Artifacts
          • 要求每条边最多只能经过一次,容易知道本题可能需要缩点。
          • 对于每个边双连通分量缩点,易知如果在一个边双连通分量中存在边权为 \(1\) 的边,则把缩成的这个点的点权赋值为 \(1\) ,然后从起点 \(c[a]\) 开始 \(DFS\) ,看到达终点 \(c[b]\) 时是否经过点权为 \(1\) 的点。然后注意一下每条边最多只能经过一次,开个 \(vis\) 数组判断即可。
          • 代码

参考资料:

学校的课件(不方便放出)

《算法竞赛进阶指南》———李煜东

强连通分量 | 割点和桥 |双连通分量

『学习笔记』Tarjan

强连通分量及缩点tarjan算法解析

『Tarjan算法 无向图的割点与割边』

【学习笔记】Tarjan的更多相关文章

  1. [学习笔记]tarjan求割点

    都口胡了求割边,就顺便口胡求割点好了QAQ 的定义同求有向图强连通分量. 枚举当前点的所有邻接点: 1.如果某个邻接点未被访问过,则访问,并在回溯后更新 2.如果某个邻接点已被访问过,则更新 对于当前 ...

  2. [学习笔记]tarjan求割边

    上午打模拟赛的时候想出了第三题题解,可是我不会求割边只能暴力判割边了QAQ 所以,本文介绍求割边(又称桥). 的定义同求有向图强连通分量. 枚举当前点的所有邻接点: 1.如果某个邻接点未被访问过,则访 ...

  3. [学习笔记]Tarjan&&欧拉回路

    本篇并不适合初学者阅读. SCC: 1.Tarjan缩点:x回溯前,dfn[x]==low[x]则缩点. 注意: ①sta,in[]标记. ②缩点之后连边可能有重边. 2.应用: SCC应用范围还是很 ...

  4. 学习笔记--Tarjan算法之割点与桥

    前言 图论中联通性相关问题往往会牵扯到无向图的割点与桥或是下一篇博客会讲的强连通分量,强有力的\(Tarjan\)算法能在\(O(n)\)的时间找到割点与桥 定义 若您是第一次了解\(Tarjan\) ...

  5. [学习笔记] Tarjan算法求桥和割点

    在之前的博客中我们已经介绍了如何用Tarjan算法求有向图中的强连通分量,而今天我们要谈的Tarjan求桥.割点,也是和上篇有博客有类似之处的. 关于桥和割点: 桥:在一个有向图中,如果删去一条边,而 ...

  6. [学习笔记] Tarjan算法求强连通分量

    今天,我们要探讨的就是--Tarjan算法. Tarjan算法的主要作用便是求一张无向图中的强连通分量,并且用它缩点,把原本一个杂乱无章的有向图转化为一张DAG(有向无环图),以便解决之后的问题. 首 ...

  7. $tarjan$简要学习笔记

    $QwQ$因为$gql$的$tarjan$一直很差所以一直想着要写个学习笔记,,,咕了$inf$天之后终于还是写了嘻嘻. 首先说下几个重要数组的基本定义. $dfn$太简单了不说$QwQ$ 但是因为有 ...

  8. Tarjan的学习笔记 求割边求割点

    博主图论比较弱,搜了模版也不会用... 所以决心学习下tarjan算法. 割点和割边的概念不在赘述,tarjan能在线性时间复杂度内求出割边. 重要的概念:时间戟,就是一个全局变量clock记录访问结 ...

  9. 仙人掌&圆方树学习笔记

    仙人掌&圆方树学习笔记 1.仙人掌 圆方树用来干啥? --处理仙人掌的问题. 仙人掌是啥? (图片来自于\(BZOJ1023\)) --也就是任意一条边只会出现在一个环里面. 当然,如果你的图 ...

  10. OI知识点|NOIP考点|省选考点|教程与学习笔记合集

    点亮技能树行动-- 本篇blog按照分类将网上写的OI知识点归纳了一下,然后会附上蒟蒻我的学习笔记或者是我认为写的不错的专题博客qwqwqwq(好吧,其实已经咕咕咕了...) 基础算法 贪心 枚举 分 ...

随机推荐

  1. Go语言安装(Windows10)

    一. 官网下载 https://golang.google.cn/dl/   二. 软件包安装 选择对应的路径进行安装   三. 环境变量设置 1.path 检查系统环境变量Path内已经添加Go的安 ...

  2. 【ThreadX-NetX Duo】Azure RTOS NetX Duo概述

    Azure RTOS NetX Duo嵌入式TCP / IP网络堆栈是Microsoft高级的工业级双IPv4和IPv6 TCP / IP网络堆栈,专门为深度嵌入式,实时和IoT应用程序设计.NetX ...

  3. 【面试题精讲】JavaOptional用法

    有的时候博客内容会有变动,首发博客是最新的,其他博客地址可能会未同步,认准https://blog.zysicyj.top 首发博客地址 文章更新计划 系列文章地址 Java 8 引入了 Option ...

  4. [转帖]手摸手搭建简单的jmeter+influxdb+grafana性能监控平台

    我安装的机器是阿里云的centos8机器,其他的系统暂未验证 1.安装influxdb influxdb 下载地址https://portal.influxdata.com/downloads/,也可 ...

  5. [转帖]如何部署windows版本的oswatcher

    2017-02-22 没有评论 windows上也有os watcher:OSWFW. 目前支持的windows版本是: Windows XP (x86 & x64)Windows 7 (x8 ...

  6. [转帖]kafka搭建kraft集群模式

    kafka2.8之后不适用zookeeper进行leader选举,使用自己的controller进行选举 1.准备工作 准备三台服务器 192.168.3.110 192.168.3.111 192. ...

  7. [转帖]基本系统调用性能lmbench测试方法和下载

    简介 Lmbench是一套简易,可移植的,符合ANSI/C标准为UNIX/POSIX而制定的微型测评工具.一般来说,它衡量两个关键特征:反应时间和带宽. Lmbench旨在使系统开发者深入了解关键操作 ...

  8. [转帖]ARMv8架构概述、相关技术文档以及ARMv8处理器简介

    ARMv8架构 文章目录 ARMv8架构 参考文档 ARMv8架构的概述 从32位到64位的变化The changes from 32 bits to 64 bits 1,Larger registe ...

  9. IPMI的简单使用

    背景 公司一台十一年前的服务器砸到我手中,要重装CentOS7的操作系统. 本着不想进机房, 不想格式化U盘的想法, 想用BMC进行安装系统. 遇到的第一个问题是不知道密码. 询问之前的机器持有人,也 ...

  10. React中Props的详细使用和props的校验

    props中的children属性 组件标签只用有子节点的时候,props就会有该属性; children的属性跟props一样的,值可以是任意值;(文本,React元素,组件,函数) 组件: < ...