zoukankan      html  css  js  c++  java
  • 【总结】强连通分量

    强连通分量总结


    概念

    来自百度

    有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图

     如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)
    极大强连通子图?

    其实就是使这个环尽量地大


    例如

    对于“极大”的理解,就是在一个局部子图中不能再大。就像是数学中的求一个函数中的极大值和极小值一样,例如求函数f(x)的极大值和极小值,变量x可以有不同的区间,所以在x的不同区间内就会有不同的极大值或极小值。

    {1,2,4} {1,3,4}是强连通的;但是,都不是强连通分量

    图中的强连通分量为{1,2,3,4} {5} {6}


    算法

    有Tarjan,Kosaraju,Gabow等

    Tarjan

    这大概是我唯一记得的了

    简单原理

    tarjan算法基于dfs算法,同一强连通分量内的所有顶点均在同一棵深度优先搜索树中。也就是说强连通分量一定是有向图的某个深度优先搜索生成树。

    用low值记录点u所在强连通子图对应的搜索子树的根节点的dfs值。该子树中的元素在栈中一定是相邻的,且根节点在栈中一定位于所有子树元素的最下方。

    强连通分量是由若干个环组成,所以当有环形成时,我们将这一条路径的low值统一,即这条路径上的所有点属于同一个强连通分量。

    如果遍历完整个搜索树后某个点的dfn值等于low值,则它是该搜索子树的根。这时,它以上(包括它自己)一直到栈顶的所有元素组成一个强连通分量。

     

    规则
    1. 数组初始化:定义两个数组dfn,dfn[i]代表第一次到达i时的时间,当首次搜索到点u时,low[i]初始化为dfn[i]

    2. 堆栈:每遍历到一个未被标记的点u,将它入栈。

    3. 再从当前点u开始遍历,当从v回溯到u时,如果点v不在栈中,那么low[u] = min{low[u],low[v]};如果点v在栈中,那么low[u] = min{low[u],dfn[v]}

    4. 每当搜索到一个点i并经过以上步骤后,如果其low[i]=dfn[i],则将它以及在它之上的元素弹出栈。这些出栈元素组成一个强连通分量。

    5. 继续搜索(因为有向图可能有多个连通子图组成,而这些子图没有交集),直到所有点被遍历。

     

    演示

    引用网上一篇博客的算法演示http://kmplayer.iteye.com/blog/604532

    从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。

    我们遍历到节点6,发现已经搜到底,不能再遍历,判断DFN[6]=LOW[6],弹出。6也是一个强连通分量

    返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量

    回溯到5,low[5] = min{low[5],low[6]},发现已无处可去,判断DFN[5]=LOW[5],将5弹出,5是一个强连通分量

    返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是 横叉边,返回3,(3,4)为 树枝边 所以LOW[3]=LOW[4]=1。

    继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。

    至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。

    可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。

    求有向图的强连通分量还有一个强有力的算法,为Kosaraju算法。Kosaraju是基于对有向图及其逆图两次DFS的方法,其时间复杂度也是O(N+M)。与Trajan算法相比,Kosaraju算法可能会稍微更直观一些。但是Tarjan只用对原图进行一次DFS,不用建立逆图,更简洁。在实际的测试中,Tarjan算法的运行效率也比Kosaraju算法高30%左右。此外,该Tarjan算法与求无向图的双连通分量(割点、桥)的Tarjan算法也有着很深的联系。学习该Tarjan算法,也有助于深入理解求双连通分量的Tarjan算法,两者可以类比、组合理解。

     

    代码

    #include<algorithm>
    #include<iostream>
    #include<cstdio>
    #include<stack>
    using namespace std;
    const int N = 105;
    int n, cnt, tim, dcnt, hd[N], dfn[N], low[N], belong[N], ru[N], chu[N];      //tim记时间,dcnt为强连通分量的数量,belong[i]=k表示点i在第k个强连通分量中
    bool instk[N];
    stack<int>stk;
    struct edge  {
      int to, nxt;
    } v[N * N];
    void addedge(int x, int y)  {
      ++cnt;
      v[cnt].to = y;
      v[cnt].nxt = hd[x];
      hd[x] = cnt;
    }
    void tarjan(int u)  {
      dfn[u] = low[u] = ++tim;
      instk[u] = 1;
      stk.push(u);
      for (int i = hd[u]; i; i = v[i].nxt)
        if (dfn[v[i].to] == 0)  {
          tarjan(v[i].to);
          low[u] = min(low[u], low[v[i].to]);
        }
        else if (instk[v[i].to]) low[u] = min(low[u], dfn[v[i].to]);
      if (low[u] == dfn[u])  {
        ++dcnt;
        while (1)  {
          int t = stk.top();
          stk.pop();
          instk[t] = 0;
          belong[t] = dcnt;
          if (t == u)  break;
        }
      }
    }
    int main()  {
      scanf("%d", &n);
      for (int i = 1; i <= n; i++)  {
        int x;
        while (scanf("%d", &x) && x) addedge(i, x);
      }
      for (int i = 1; i <= n; i++)  if (dfn[i] == 0)  tarjan(i);
      printf("%d",dcnt);
      return 0;
    

     

    举例

    校园网
    传送门

    题目

    一些学校连入一个电脑网络。那些学校已订立了协议:每个学校都会给其它的一些学校分发软件(称作“接受学校”)。注意即使 B 在 A 学校的分发列表中, A 也不一定在 B 学校的列表中。

    你要写一个程序计算,根据协议,为了让网络中所有的学校都用上新软件,必须接受新软件副本的最少学校数目(子任务 A)。更进一步,我们想要确定通过给任意一个学校发送新软件,这个软件就会分发到网络中的所有学校。为了完成这个任务,我们可能必须扩展接收学校列表,使其加入新成员。计算最少需要增加几个扩展,使得不论我们给哪个学校发送新软件,它都会到达其余所有的学校(子任务 B)。一个扩展就是在一个学校的接收学校列表中引入一个新成员。

     

    输入格式:

    输入文件的第一行包括一个整数 N:网络中的学校数目(2 <= N <= 100)。学校用前 N 个正整数标识。

    接下来 N 行中每行都表示一个接收学校列表(分发列表)。第 i+1 行包括学校 i 的接收学校的标识符。每个列表用 0 结束。空列表只用一个 0 表示。

     

    输出格式:

    你的程序应该在输出文件中输出两行。

    第一行应该包括一个正整数:子任务 A 的解。

    第二行应该包括子任务 B 的解。

     

    输入样例
    5
    2 4 3 0
    4 5 0
    0
    0
    1 0
    

     

    输出样例
    1
    2
    

     

    思路

    对于子任务A 用Tarjan缩点后,入度为0的点即为子任务A的解

    对于子任务B 求入度为0的点个数与出度为0的点个数的最大值 需要添多少条边才能形成使所有点都可以被某个点到达 所以对于每个出度为0的点 需要拓展一条出边 对于每个入度为0的点 需要拓展一条入边 为了减少拓展边数 可以将所有出度为0的点拓展出边到某个入度为0的点的上 最后答案即为 max(入度为0点数,出度为0点数)

     

    代码
    #include<algorithm>
    #include<iostream>
    #include<cstdio>
    #include<stack>
    using namespace std;
    const int N = 105;
    int n, cnt, tim, dcnt, cnt1, cnt2, ans1, ans2, hd[N], dfn[N], low[N], belong[N], ru[N], chu[N];
    bool instk[N];
    stack<int>stk;
    struct edge  {
      int to, nxt;
    } v[N * N];
    void addedge(int x, int y)  {
      ++cnt;
      v[cnt].to = y;
      v[cnt].nxt = hd[x];
      hd[x] = cnt;
    }
    void tarjan(int u)  {
      dfn[u] = low[u] = ++tim;
      instk[u] = 1;
      stk.push(u);
      for (int i = hd[u]; i; i = v[i].nxt)
        if (dfn[v[i].to] == 0)  {
          tarjan(v[i].to);
          low[u] = min(low[u], low[v[i].to]);
        }
        else if (instk[v[i].to]) low[u] = min(low[u], dfn[v[i].to]);
      if (low[u] == dfn[u])  {
        ++dcnt;
        while (1)  {
          int t = stk.top();
          stk.pop();
          instk[t] = 0;
          belong[t] = dcnt;
          if (t == u)  break;
        }
      }
    }
    int main()  {
      freopen("schlnet.in", "r", stdin);
      freopen("schlnet.out", "w", stdout);
      scanf("%d", &n);
      for (int i = 1; i <= n; i++)  {
        int x;
        while (scanf("%d", &x) && x) addedge(i, x);
      }
      for (int i = 1; i <= n; i++)  if (dfn[i] == 0)  tarjan(i);
      for (int i = 1; i <= n; i++)
        for (int j = hd[i]; j; j = v[j].nxt)
          if (belong[i] != belong[v[j].to]) ru[belong[v[j].to]]++, chu[belong[i]]++;
      for (int i = 1; i <= dcnt; i++)  {
        if (ru[i] == 0) ans1++, cnt1++;
        if (chu[i] == 0)  cnt2++;
      }
      ans2 = max(cnt1, cnt2);
      if (dcnt == 1) ans2 = 0;
      printf("%d
    %d
    ", ans1, ans2);
      return 0;
    }
    

     


    Kosaraju和Gabow

    等我想起来,再说吧


    总结

    还早着呢

  • 相关阅读:
    数据对象映射模式
    策略模式
    适配模式
    注册模式
    单例模式
    工厂模式
    PHP魔法方法的使用
    【转】通过 ulimit 改善系统性能
    HDMI相关知识
    中国三种3G网络频段
  • 原文地址:https://www.cnblogs.com/bbqub/p/7750449.html
Copyright © 2011-2022 走看看