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

    摘自《挑战程序设计》4.3.1

    【强连通分量分解原理】

      对于一个有向图顶点的子集S,如果在S内任取两个顶点u和v,都能找到一条从u到v的路径,那么就称S是强连通的。如果在强连通的顶点集合S中加入其他任意顶点集合后,它都不再是强连通的,那么就称S是原图的一个强连通分量(SCC: Strongly Connected Component)。任意有向图都可以分解成若干不相交的强连通分量,这就是强连通分量分解。把分解后的强连通分量缩成一个顶点,就得到了一个DAG(有向无环图)

      强连通分量分解可以通过两次简单的DFS实现。

      第一次DFS时,选取任意顶点作为起点,遍历所有尚未访问过的顶点,并在回溯前给顶点标号(post order,后序遍历)。对剩余的未访问过的顶点,不断重复上述过程。完成标号后,越接近图的尾部(搜索树的叶子),顶点的标号越小

      第二次DFS时,先将所有边反向,然后以标号最大的顶点为起点进行DFS。这样DFS所遍历的顶点集合就构成了一个强连通分量。之后,只要还有尚未访问的顶点,就从中选取标号最大的顶点不断重复上述过程。

      正如前文所述,我们可以将强连通分量缩点并得到DAG。此时可以发现,标号最大的节点就属于DAG头部(搜索树的根)的强连通分量。因此,将边反向后,就不能沿边访问到这个强连通分量以外的顶点。而对于强连通分量内的其他顶点,其可达性不受边反向的影响,因此在第二次DFS时,我们可以遍历一个强连通分量里的所有顶点。

      该算法只进行了两次DFS,因而总的复杂度是O(|V|+|E|)。

    算法模板如下:

     1 #include<cstdio>
     2 #include<cstdlib>
     3 #include<cstring>
     4 #include<vector>
     5 using namespace std;
     6 const int MAX_V = 10005;
     7 const int MAX_E = 50005;
     8 
     9 int V; // 顶点数
    10 vector<int> G[MAX_V];   // 图的邻接表表示
    11 vector<int> rG[MAX_V];  // 把边反向后的图
    12 vector<int> vs;         // 后序遍历顺序的顶点列表
    13 bool used[MAX_V];       // 访问标记
    14 int cmp[MAX_V];         // 所属强连通分量的拓扑序
    15 
    16 void add_edge(int from, int to)
    17 {
    18     G[from].push_back(to);
    19     rG[to].push_back(from);
    20 }
    21 
    22 void dfs(int v)
    23 {
    24     used[v] = true;
    25     for (int i = 0; i < G[v].size(); i++)
    26     {
    27         if (!used[G[v][i]]) dfs(G[v][i]);
    28     }
    29     vs.push_back(v);
    30 }
    31 
    32 void rdfs(int v, int k)
    33 {
    34     used[v] = true;
    35     cmp[v] = k;
    36     for (int i = 0; i < rG[v].size(); i++)
    37     {
    38         if (!used[rG[v][i]]) rdfs(rG[v][i], k);
    39     }
    40 }
    41 
    42 int scc()
    43 {
    44     memset(used, 0, sizeof(used));
    45     vs.clear();
    46     for (int v = 0; v < V; v++)
    47     {
    48         if (!used[v]) dfs(v);
    49     }
    50     memset(used, 0, sizeof(used));
    51     int k = 0;
    52     for (int i = vs.size() - 1; i >= 0; i--)
    53     {
    54         if (!used[vs[i]]) rdfs(vs[i], k++);
    55     }
    56     return k;
    57 }

    【入门】POJ 2186 -- Popular Cows

    题意:

      每头牛都想成为牛群中的红人。给定N头牛的牛群和M个有序对(A, B)。(A, B)表示牛A认为牛B是红人。该关系具有传递性,所以如果牛A认为牛B是红人,牛B认为牛C是红人,那么牛A也认为牛C是红人。不过,给定的有序对中可能包含(A, B)和(B, C),但不包含(A, C)。求被其他所有牛认为是红人的牛的总数。

    分析:

      考虑以牛为顶点的有向图,对每个有序对(A, B)连一条从 A到B的有向边。那么,被其他所有牛认为是红人的牛对应的顶点,也就是从其他所有顶点都可达的顶点。虽然这可以通过从每个顶点出发搜索求得,但总的复杂度却是O(NM),是不可行的,必须要考虑更为高效的算法。

      假设有两头牛A和B都被其他所有牛认为是红人。那么显然,A被B认为是红人,B也被A认为是红人,即存在一个包含A、B两个顶点的圈,或者说,A、B同属于一个强连通分量。反之,如果一头牛被其他所有牛认为是红人,那么其所属的强连通分量内的所有牛都被其他所有牛认为是红人

      由此,我们把图进行强连通分量分解后,至多有一个强连通分量满足题目的条件。而按前面介绍的算法进行强连通分量分解时,我们还能够得到各个强连通分量拓扑排序后的顺序,唯一可能成为解的只有拓扑序最后的强连通分量。所以在最后,我们只要检查这个强连通分量是否从所有顶点可达就好了。该算法的复杂度为O(N+M),足以在时限内解决原题。

    代码:

      1 #include<cstdio>
      2 #include<cstdlib>
      3 #include<cstring>
      4 #include<vector>
      5 using namespace std;
      6 const int MAX_V = 10005;
      7 const int MAX_M = 50005;
      8 int M, N;
      9 int A[MAX_M], B[MAX_M];
     10 
     11 vector<int>  G[MAX_V]; // 图的邻接表表示
     12 vector<int> rG[MAX_V]; // 把边反向后的图
     13 vector<int> vs;        // 后序遍历顺序的顶点列表
     14 bool used[MAX_V];      // 访问标记
     15 int SCC[MAX_V];        // 所属强连通分量的拓扑序
     16 void add_edge(int from, int to)
     17 {
     18      G[from].push_back(to);
     19     rG[to].push_back(from);
     20 }
     21 void dfs(int u) //第一次dfs,后序遍历标记,越靠近叶子结点标号越小
     22 {
     23     used[u] = true;
     24     for(int i = 0; i < G[u].size(); i++)
     25     {
     26         int v = G[u][i];
     27         if(!used[v]) dfs(v);
     28     }
     29     vs.push_back(u);
     30 }
     31 void rdfs(int u, int k) //反向dfs,利用反向图,求出强连通分量个数
     32 {
     33     used[u] = true;
     34     SCC[u] = k;
     35     for(int i = 0; i < rG[u].size(); i++)
     36     {
     37         int v = rG[u][i];
     38         if(!used[v]) rdfs(v, k);
     39     }
     40 }
     41 
     42 int scc()
     43 {
     44     memset(used, 0, sizeof(used));
     45     vs.clear();
     46     for(int v = 0; v < N; v++)
     47         if(!used[v]) dfs(v);
     48 
     49     memset(used, 0, sizeof(used));
     50     int k = 0; //DAG结点个数
     51     for(int i = vs.size()-1; i >= 0; i--)
     52     {
     53         int v = vs[i];
     54         if(!used[v]) rdfs(v, k++);
     55     }
     56     return k;
     57 }
     58 void init()
     59 {
     60     for(int i = 0; i < MAX_V; i++)
     61     {
     62          G[i].clear();
     63         rG[i].clear();
     64     }
     65 }
     66 int solve()
     67 {
     68     for(int i = 0; i < M; i++)
     69     {
     70         add_edge(A[i]-1, B[i]-1);
     71     }
     72     int n = scc();
     73 
     74     int V = N;
     75     int u = 0, num = 0; //num为最末强连通分量中的结点个数
     76     for(int v = 0; v < V; v++)
     77     {
     78         if(SCC[v] == n-1)
     79         {
     80             u = v; num++;
     81         }
     82     }
     83 
     84     //检查是否所有点均可达u
     85     memset(used, 0, sizeof(used));
     86     rdfs(u, 0);  //从叶子结点往前搜索
     87     for(int v = 0; v < V; v++)
     88     {
     89         if(!used[v])
     90         {
     91             num = 0;
     92             break;
     93         }
     94     }
     95     return num;
     96 }
     97 
     98 int main()
     99 {
    100     init();
    101     scanf("%d%d", &N, &M);
    102     for(int i = 0; i < M; i++)
    103     {
    104         scanf("%d%d", &A[i], &B[i]);
    105     }
    106     printf("%d
    ", solve());
    107     return 0;
    108 }
    POJ 2186
  • 相关阅读:
    015.Python基础--模块
    014.Python基础--格式化输入输出
    013.Python基础--异常/错误处理
    012.Python基础--装饰器深入
    011.Python基础--装饰器
    010.Python基础--生成器
    汇编的角度分析指针-03(字符串深入理解)
    汇编的角度分析C语言的指针-02
    汇编的角度分析C语言的switch语句
    分析C语言的字节对齐
  • 原文地址:https://www.cnblogs.com/LLGemini/p/4725952.html
Copyright © 2011-2022 走看看