【强连通分量分解】

Posted on 2015-08-13 00:05  LLGemini  阅读(500)  评论(0编辑  收藏  举报

摘自《挑战程序设计》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\n", solve());
107     return 0;
108 }
POJ 2186