【强连通分量分解】

摘自《挑战程序设计》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

时间: 2024-10-24 16:17:33

【强连通分量分解】的相关文章

强连通分量分解 tarjan算法 (hdu 1269)

题意: 给出一个有n个点m条边的有向图,判断该图是否只有一个强连通分量. 限制: 0 <= N <= 10000 0 <= M <= 100000 思路: tarjan算法分解强连通分量. /*强连通分量分解 tarjan算法 (hdu 1269) 题意: 给出一个有n个点m条边的有向图,判断该图是否只有一个强连通分量. 限制: 0 <= N <= 10000 0 <= M <= 100000 */ #include<iostream> #inc

POJ(2186)强连通分量分解

#include<cstdio> #include<vector> #include<cstring> using namespace std; const int MAX_N=10005; vector<int> G[MAX_N]; vector<int> rG[MAX_N];//存储边反向之后的图 vector<int> PostOrder;//存储图的后序遍历 int V,E; bool used[MAX_N]; int com

强连通分量分解

#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <vector> using namespace std; const int N = 10000 + 10; const int M = 50000 + 10; int n, m; int head1[N], tot1, head2[N], tot2; bool vis[N

POJ2186(强连通分量分解)

Popular Cows Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 35035   Accepted: 14278 Description Every cow's dream is to become the most popular cow in the herd. In a herd of N (1 <= N <= 10,000) cows, you are given up to M (1 <= M &

强连通分量分解 Kosaraju算法 (poj 2186 Popular Cows)

poj 2186 Popular Cows 题意: 有N头牛, 给出M对关系, 如(1,2)代表1欢迎2, 关系是单向的且可以传递, 即1欢迎2不代表2欢迎1, 但是如果2也欢迎3那么1也欢迎3. 求被所有牛都欢迎的牛的数量. 限制: 1 <= N <= 10000 1 <= M <= 50000 思路: Kosaraju算法, 看缩点后拓扑序的终点有多少头牛, 且要判断是不是所有强连通分量都连向它. Kosaraju算法,分拆完连通分量后,也完成了拓扑序. /*poj 2186

强连通分量tarjan模板复习

对于一个有向图定点的子集,在该子集中任取两点u与v,都能找到一条从u到v的路径,则称该子集是强连通的.若该集合加入到任意点集中,它都不再强连通,则称这个子集是原图的一个强连通分量.任意一张图都可以分解成若干个不相交的强连通分量.这是强连通分量分解.把分解后的强连通分量缩成一个顶点,就可以得到一个有向无环图. 如图: 求一张图的强连通分量的个数,常用tarjan算法,它是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树.搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈

poj2186Popular Cows(Kosaraju算法--有向图的强连通分量的分解)

1 /* 2 题目大意:有N个cows, M个关系 3 a->b 表示 a认为b popular:如果还有b->c, 那么就会有a->c 4 问最终有多少个cows被其他所有cows认为是popular! 5 6 思路:强连通分量中每两个节点都是可达的! 通过分解得到最后一个连通分量A, 7 如果将所有的强连通分量看成一个大的节点,那么A一定是孩子节点(因为我们先 8 完成的是父亲节点的强连通分量)! 最后如果其他的强连通分量都可以指向A,那么 9 A中的每一个cow都会被其他cows所

Tarjan算法分解强连通分量(附详细参考文章)

Tarjan算法分解强连通分量 算法思路: 算法通过dfs遍历整个连通分量,并在遍历过程中给每个点打上两个记号:一个是时间戳,即首次访问到节点i的时刻,另一个是节点u的某一个祖先被访问的最早时刻. 时间戳用DFN数组存储,最早祖先用low数组来存,每次dfs遍历到一个节点u,即让这两个记号等于当前时刻,在后面回溯或者判断的过程中在来更新low,DNF是一定的,因为第一次访问时刻一定.然后遍历u的子节点,也就是跟u相连的点v,依次看子节点的时间戳有没有打上,也就是看他有没有被访问过.\(1\).没

POJ_2186_Popular Cows_强连通分量

Popular Cows Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 30680   Accepted: 12445 Description Every cow's dream is to become the most popular cow in the herd. In a herd of N (1 <= N <= 10,000) cows, you are given up to M (1 <= M &