【强连通分量分解原理】
对于一个有向图顶点的子集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 }