刚学到强连通分支及其缩点,瞻仰了下牛人博客: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算法,百度百科有介绍。

posted on 2010-08-06 09:25  ylfdrib  阅读(1100)  评论(0编辑  收藏  举报