pku1236 2186 2553强连通分支及其缩点(Tarjan算法)

刚学到强连通分支及其缩点,瞻仰了下牛人博客:BYVoid, 这里图文并茂,非常清晰的讲了Tarjan算法的流程,很快我就看懂了!看来,牛人就是不一样啊。我感觉Tarjan还是蛮不错的,时间复杂度在O(n + m),已经很低了。而且这位大牛提供的伪码相当不错,一看便懂!

找了三道题刷了一下,感觉这三道题都差不多,主要部分代码相同,唯有问题不一样,最后不同的处理即可。

pku1236 Network of Schools

题目大意:有N个学校,每个学校都负责一部分学校的软件供给。如果该学校有新软件用,它负责的那几个学校也可以copy一份用。

问1:现有一款软件,至少需要给多少个学校发布,才能使得所有的学校都能用上这款软件。

问2:如果给任何一个学校送去新软件,所有学校都能用上,最少需要再添加几个这种学校u与学校v的附属关系。

代码
   
     
1 /*
2 问题转化:
3 subtask1: 求最少的学校数,使得能够覆盖所有的学校
4 ---> 求图中入度为0的结点个数
5 subtask2: 求需要增加的最少的学校列表,使得从任何一个学校发布软件,其他学校都能收到
6 ---> 求图中入度为0的节点数和出度为0的节点数中的较大者
7   */
8 #include < stdio.h >
9 #include < string .h >
10   #define NN 104
11 #define MM 10010
12 typedef struct node{
13 int v;
14 struct node * nxt;
15 }NODE;
16
17 NODE edg[MM];
18 NODE * link[NN];
19
20 int N, Index, top, Bcnt, edNum;
21 int DFN[NN]; // 结点的搜索次序号,或叫时间戳
22 int LOW[NN]; // u或u的子树能够追溯到的最早的栈中节点的次序号
23 int stack[NN]; // 保存已搜到且还未归入某一强连通分支的结点
24 int belong[NN]; // 结点u的归属分支
25 char in [NN]; // 标记分支i的入度是否为0
26 char out [NN]; // 标记分支i的出度是否为0
27 char inStack[NN]; // 标记结点是否在栈中
28
29 int Min( int a, int b){
30 return a < b ? a : b;
31 }
32 int Max( int a, int b){
33 return a > b ? a : b;
34 }
35 void Add( int u, int v){ // 加边,邻接表存
36 edg[edNum].v = v;
37 edg[edNum].nxt = link[u];
38 link[u] = edg + edNum ++ ;
39 }
40
41 void Tarjan( int u) // tarjan算法求强连通分支
42 { // 判断节点u是否为某一强连通分支的根节点,或说某一分支编号最小的点
43 int v;
44 DFN[u] = LOW[u] = ++ Index;
45 stack[ ++ top] = u;
46 inStack[u] = 1 ;
47 for (NODE * p = link[u]; p; p = p -> nxt){ // 遍历所有u节点的子结点
48 v = p -> v;
49 if ( ! DFN[v]){
50 Tarjan(v);
51 LOW[u] = Min(LOW[u], LOW[v]);
52 } else if (inStack[v]){
53 LOW[u] = Min(LOW[u], DFN[v]);
54 }
55 }
56 if (DFN[u] == LOW[u]){
57 Bcnt ++ ; // 分支的个数加一
58 do {
59 v = stack[top -- ];
60 inStack[v] = 0 ;
61 belong[v] = Bcnt; // 将栈中节点归属到当前分支
62 } while (v != u);
63 }
64 }
65 int main()
66 {
67 int i, x, u, v;
68 int zeroIn, zeroOut; // 记录入度为0,出度为0的节点个数
69 scanf( " %d " , & N);
70 memset(link, 0 , sizeof (link));
71 for (i = 1 ; i <= N; i ++ ){
72 while (scanf( " %d " , & x) != EOF){
73 if (x == 0 ) break ;
74 Add(i, x); // 加边
75 }
76 }
77 Index = top = Bcnt = 0 ;
78 memset(DFN, 0 , sizeof (DFN));
79 memset(inStack, 0 , sizeof (inStack));
80 for (i = 1 ; i <= N; i ++ ){
81 if ( ! DFN[i]){
82 Tarjan(i);
83 }
84 }
85 memset( out , 0 , sizeof ( out ));
86 memset( in , 0 , sizeof ( in ));
87 for (i = 1 ; i <= N; i ++ ){
88 for (NODE * p = link[i]; p; p = p -> nxt){
89 u = belong[i]; v = belong[p -> v];
90 if (u != v){ // 如果不在同一分支中
91 out [u] = 1 ;
92 in [v] = 1 ;
93 }
94 }
95 }
96 zeroIn = zeroOut = 0 ;
97 for (i = 1 ; i <= Bcnt; i ++ ){
98 if ( ! in [i]) zeroIn ++ ;
99 if ( ! out [i]) zeroOut ++ ;
100 }
101 printf( " %d\n%d\n " , zeroIn, Bcnt == 1 ? 0 : Max(zeroIn, zeroOut));
102 return 0 ;
103 }
104

pku2186 Popular Cows

 

题意:有N头奶牛,M对<A, B>,表示A奶牛认为B奶牛很popular,而且这种关系具有传递性,即如果<A, B>,且<B, C>,则<A, C>。

问:有多少奶牛被其他所有奶牛都认为很popular。

分析:如果把所有边反向,则问题转化为:有多少奶牛认为其他奶牛都很popular,这样很容易就想到,将分支压成缩点后,反向图中入度为0的分支即为所求,且入度为0的点必须唯一,否者,ans = 0, 同样,只要求得原图中出度为0,且唯一的分支即可。

 

代码
   
     
1 /*
2 问题转化:
3 求被所有cow都认为popular的cow有多少
4 ---> 如果只有一个分支出度为0,则这个分支中的结点数即为所求,否者 ans = 0
5 */
6 #include < stdio.h >
7 #include < string .h >
8 #define NN 10004
9 #define MM 50004
10
11 typedef struct node{
12 int v;
13 struct node * nxt;
14 }NODE;
15 NODE edg[MM];
16 NODE * link[NN];
17 int N, M;
18 int idx; // 边的总数
19 int scc; // 强连通分支个数
20 int top; // 栈顶
21 int time; // 时间戳,每个节点的访问次序编号
22
23 int cnt[NN]; // 标记强连通分支i中节点数
24 int num[NN]; // 标记结点i的时间戳(访问次序号)
25 int low[NN]; // 记录结点u或u的子树中所有节点的最小标号
26 int stack[NN];
27 int inSta[NN]; // 标记结点是否在栈中
28 int out [NN]; // 标记分支i是否有出度
29 int root[NN]; // 标记结点i属于哪个分支
30
31 int Min( int a, int b){
32 return a < b ? a : b;
33 }
34
35 void Add( int u, int v){ // 加边
36 edg[idx].v = v;
37 edg[idx].nxt = link[u];
38 link[u] = edg + idx ++ ;
39 }
40 void Tarjan( int u){
41 int v;
42 num[u] = low[u] = ++ time;
43 stack[ ++ top] = u;
44 inSta[u] = 1 ;
45 for (NODE * p = link[u]; p; p = p -> nxt){
46 v = p -> v;
47 if (num[v] == 0 ){ // 未访问
48 Tarjan(v);
49 low[u] = Min(low[u], low[v]);
50 } else if (inSta[v]){
51 low[u] = Min(low[u], num[v]);
52 }
53 }
54 if (num[u] == low[u]){
55 scc ++ ;
56 do {
57 v = stack[top -- ];
58 inSta[v] = 0 ;
59 root[v] = scc;
60 cnt[scc] ++ ;
61 } while (v != u);
62 }
63 }
64 void Sovle(){
65 int i, u, v;
66 int out0; // 记录出度为0的分支的个数
67 int flag; // 记录结果,哪个分支能其余分支都能到达
68 memset(num, 0 , sizeof (num));
69 memset(cnt, 0 , sizeof (cnt));
70 time = scc = top = 0 ;
71 for (i = 1 ; i <= N; i ++ ){
72 if ( ! num[i]){
73 Tarjan(i);
74 }
75 }
76 memset( out , 0 , sizeof ( out ));
77 for (i = 1 ; i <= N; i ++ ){
78 for (NODE * p = link[i]; p; p = p -> nxt){
79 u = root[i]; v = root[p -> v];
80 if (u != v){
81 out [u] = 1 ;
82 }
83 }
84 }
85 out0 = 0 ;
86 for (i = 1 ; i <= scc; i ++ ){
87 if ( ! out [i]){
88 flag = i;
89 out0 ++ ;
90 }
91 }
92 if (out0 == 1 ){
93 printf( " %d\n " , cnt[flag]);
94 } else {
95 puts( " 0 " );
96 }
97 }
98 int main()
99 {
100 int i, a, b;
101 scanf( " %d%d " , & N, & M);
102 idx = 0 ;
103 memset(link, 0 , sizeof (link));
104 for (i = 1 ; i <= M; i ++ ){
105 scanf( " %d%d " , & a, & b);
106 Add(a, b);
107 }
108 Sovle();
109 return 0 ;
110 }
111

pku2553 The Bottom of a Graph

分析:很规范的一个关于图的定义,这题中新定义了一个名词:A node v in a graph G=(V,E) is called a sink, if for every node w in G that is reachable from v, v is also reachable from w.The bottom of a graph is the subset of all nodes that are sinks。

sink:所有能到达v的结点w,v同样能到达w,则称v为sink。

问题求得就是图中所有sink的集合,并按升序输出。

代码
   
     
1 /*
2 问题转化:
3 对于v能到达的任意一点w,w也能到达v,则v称为sink,求这所有的sink的点集,按升序输出
4 ---> 求所有出度为0的分支,按序输出这些分支中的结点
5 */
6 #include < stdio.h >
7 #include < string .h >
8 #define NN 5004
9 #define MM 50004
10
11 typedef struct node{
12 int v;
13 struct node * nxt;
14 }NODE;
15 NODE edg[MM];
16 NODE * link[NN];
17 int N, M;
18 int idx; // 边的总数
19 int scc; // 强连通分支个数
20 int top; // 栈顶
21 int time; // 时间戳,每个节点的访问次序编号
22
23 int cnt[NN]; // 标记强连通分支i中节点数
24 int num[NN]; // 标记结点i的时间戳(访问次序号)
25 int low[NN]; // 记录结点u或u的子树中所有节点的最小标号
26 int stack[NN];
27 int inSta[NN]; // 标记结点是否在栈中
28 int out [NN]; // 标记分支i是否有出度
29 int root[NN]; // 标记结点i属于哪个分支
30
31 int Min( int a, int b){
32 return a < b ? a : b;
33 }
34
35 void Add( int u, int v){ // 加边
36 edg[idx].v = v;
37 edg[idx].nxt = link[u];
38 link[u] = edg + idx ++ ;
39 }
40 void Tarjan( int u){
41 int v;
42 num[u] = low[u] = ++ time;
43 stack[ ++ top] = u;
44 inSta[u] = 1 ;
45 for (NODE * p = link[u]; p; p = p -> nxt){
46 v = p -> v;
47 if (num[v] == 0 ){ // 未访问
48 Tarjan(v);
49 low[u] = Min(low[u], low[v]);
50 } else if (inSta[v]){
51 low[u] = Min(low[u], num[v]);
52 }
53 }
54 if (num[u] == low[u]){
55 scc ++ ;
56 do {
57 v = stack[top -- ];
58 inSta[v] = 0 ;
59 root[v] = scc;
60 cnt[scc] ++ ;
61 } while (v != u);
62 }
63 }
64 void Sovle(){
65 int i, u, v, k;
66 int flag[NN]; // 标记出度为0的分支
67 memset(num, 0 , sizeof (num));
68 memset(cnt, 0 , sizeof (cnt));
69 time = scc = top = 0 ;
70 for (i = 1 ; i <= N; i ++ ){
71 if ( ! num[i]){
72 Tarjan(i);
73 }
74 }
75 memset( out , 0 , sizeof ( out ));
76 for (i = 1 ; i <= N; i ++ ){
77 for (NODE * p = link[i]; p; p = p -> nxt){
78 u = root[i]; v = root[p -> v];
79 if (u != v){
80 out [u] = 1 ;
81 }
82 }
83 }
84 memset(flag, 0 , sizeof (flag));
85 for (i = 1 ; i <= scc; i ++ ){
86 if ( ! out [i]){ // 统计出度为0的分支
87 flag[i] = 1 ;
88 }
89 }
90 k = 0 ; // 用来输出第一个点,保证每两个点间一个空格
91 for (i = 1 ; i <= N; i ++ ){
92 if (flag[root[i]]){ // 如果i结点所在分支出度为0
93 if (k) printf( " %d " , i);
94 else {
95 k = 1 ;
96 printf( " %d " , i);
97 }
98 }
99 }
100 puts( "" );
101 }
102 int main()
103 {
104 int i, a, b;
105 while (scanf( " %d " , & N) != EOF)
106 {
107 if (N == 0 ) break ;
108 scanf( " %d " , & M);
109 idx = 0 ;
110 memset(link, 0 , sizeof (link));
111 for (i = 1 ; i <= M; i ++ ){
112 scanf( " %d%d " , & a, & b);
113 Add(a, b);
114 }
115 Sovle();
116 }
117 return 0 ;
118 }
119

这三个问题中,中心思想一样的,都用的是Tarjan算法。这是个深搜的过程,中间可以记录的信息包括:

1. 将强连通分支缩成点,知道每个节点所属分支,以及每个分支里的结点个数。

2. 确定每个分支与其余分支的关系。

3. 缩点以后,可以对新的有向无回路图,进行很方便处理,查询某些信息。

一点点总结,希望会有用,仍需努力啊,强连通分支还有两种算法,Kosaraju算法和Gabow算法,百度百科有介绍。

你可能感兴趣的:(tar)