图的强连通&tarjan算法
强连通图:如果有向图G中的任意两个点u,v是互相可到达的,则称图G为强连通图。否则G为非强连通图。
强连通分量:若有向图G为非强连通图,它的子图G' 是强连通图,则称G' 为G的强连通分量。(极大强连通子图)
返图:将有向图G中的所有边的方向逆置,即u->v变为v->u
定理:对于一个有向图G,按照dfs的后序遍历到的点,对图G的返图进行一次dfs,就能得到其中一个强联通分量。首先,对原图dfs能连通这些点,然后对返图也能dfs到的点,是一个强连通分量。
求强连通分量的作用 :
把有向图中具有相同性质的点找出来,形成一个集合(缩点),建立缩图,能够方便地进行其它操作,而且时间效率会大大地提高,原先对多个点的操作可以简化为对它们所属的缩点的操作。 求强连通分量常常用于求拓扑排序之前,因为原图往往有环,无法进行拓扑排序,而求强连通分量后所建立的缩图则是有向无环图,方便进行拓扑排序。 缩点操作在这篇中用过(最小树形图):点击打开链接
以下主要内容:
kosaraju算法求强连通分量
tarjan算法求强连通分量、割点、桥
一、kosaraju算法。
1、基本思路
先对原图进行一次深搜,记录下访问的后序遍历顺序,即每个点在dfs的离开时间,可用栈存储。然后按照离开时间进行第二次深搜,搜返图,在第一次深搜中能到达的点,若在返图的深搜中也能到达,说明这些点是一个强联通分量。
【代码 hdu1269】:
1. <span style="font-size:18px;">#include <stdio.h>
2. #include <stdlib.h>
3. #include <string.h>
4. #include <iostream>
5. #include <algorithm>
6. #define mset(a,i) memset(a,i,sizeof(a))
7. using namespace std;
8. const int MAX=1e5+5;
9. struct node{
10. int s,t,next;
11. }e[MAX<<3];
12. int head[MAX],head2[MAX],cnt;//存原图,返图
13. bool vis[MAX];//标记访问的节点
14. int sta[MAX],top;//记录dfs后序
15. int Scc[MAX];//可以记录每个节点的分量归属
16. void add(int u,int v)//建图
17. {
18. e[cnt]=node{u,v,head[u]};
19. head[u]=cnt++;
20. e[cnt]=node{v,u,head[v]};
21. //返图
22. }
23. void init()
24. {
25. mset(head,-1);
26. mset(head2,-1);
27. cnt=0;
28. }
29. void dfs1(int u)
30. {
31. vis[u]=1;
32. for(int i=head[u];~i;i=e[i].next)
33. {
34. int t=e[i].t;
35. if(!vis[t])dfs1(t);
36. }
37. //后序
38. }
39. void dfs2(int u,int sig)
40. {
41. vis[u]=1;
42. Scc[u]=sig;
43. for(int i=head2[u];~i;i=e[i].next)
44. {
45. int t=e[i].t;
46. if(!vis[t])dfs2(t,sig);
47. }
48. }
49. int kosaraju(int n)
50. {
51. top=0;
52. mset(vis,0);
53. for(int i=1;i<=n;i++)
54. {
55. if(!vis[i])dfs1(i);
56. //第一次dfs跑原图记离开时间
57. int sig=0;
58. mset(vis,0);
59. for(int i=top-1;i>=0;i--)
60. {
61. if(!vis[sta[i]])
62. {
63. //连通分支+1
64. }
65. //第2次dfs跑返图求连通分量数
66. return sig;
67. }
68. int main()
69. {
70. int n,m,u,v;
71. while(cin>>n>>m,n)
72. {
73. init();
74. while(m--)
75. {
76. "%d%d",&u,&v);
77. add(u,v);
78. }
79. int ans=kosaraju(n);
80. if(ans==1)puts("Yes");
81. else puts("No");
82. }
83. return 0;
84. }</span>
二、tarjan算法。
1、强连通分量(有向图)
增加两个数组,dfn[]表是dfs到达每个节点的时间。low[]记录当前点或其子树中的点能到达的拥有最小时间戳的点。
若一个点dfs过程中碰到了已访问过的点,说明形成环,dfs回溯回去,回到这个环中时间戳最小的点,
一定有dfn[ u ] == low[ u ],从该点开始将栈中它的后续点依次出栈,即为一个连通分量
【代码 uva12167】:
给定有向图,问需要加几条边可以强连通?先用tarjan算强连通数,然后用缩点法计算连通块之间的入度、出度,缺多少。取max(in, out)
1. #include<stdio.h>
2. #include<stdlib.h>
3. #include<string.h>
4. #include<iostream>
5. #include<algorithm>
6. #include<map>
7. using namespace std;
8. const int MAX=202020;
9. struct node{
10. int s,t,next;
11. }e[MAX];
12. int head[MAX],cnt;
13. void add(int u,int v)
14. {
15. e[cnt]=node{u,v,head[u]};
16. head[u]=cnt++;
17. }
18. int dfn[MAX];//每个节点的访问时间编号
19. int low[MAX];//每个点能到达的最小编号
20. int sta[MAX],top;
21. int Scc[MAX];//每个点所属的分量 序号
22. int in[MAX],out[MAX];
23. void tardfs(int u,int &lay,int &sig)
24. {
25. //到达此点的时间
26. //压入栈
27. for(int i=head[u];~i;i=e[i].next)
28. {
29. int t=e[i].t;
30. if(dfn[t]==0)
31. {
32. tardfs(t,lay,sig);
33. low[u]=min(low[u],low[t]);
34. }
35. else if(!Scc[t])//访问过了
36. //强连通求法可以用low
37. }
38. if(low[u]==dfn[u])//u不能到达任何之前走过的点
39. {
40. sig++;
41. while(1)
42. {
43. int j=sta[--top];//出栈
44. Scc[j]=sig;
45. if(j==u)break;
46. //包含u的连通分量出栈
47. }
48. }
49. int tarjan(int n)
50. {
51. int sig=0;//强连通数
52. int lay=1;//时间戳
53. top=0;
54. sizeof(Scc));
55. sizeof(dfn));//时间戳归0
56. for(int i=1;i<=n;i++)
57. {
58. if(!dfn[i])tardfs(i,lay,sig);
59. }
60. return sig;//返回连通数
61. }
62. int main()
63. {
64. int n,m,u,v,T;
65. cin>>T;
66. while(T--)
67. {
68. cin>>n>>m;
69. sizeof(head));
70. cnt=0;
71. while(m--)
72. {
73. "%d%d",&u,&v);
74. add(u,v);
75. }
76. int sig=tarjan(n);
77. if(sig==1)puts("0");
78. else
79. {
80. sizeof(in));
81. sizeof(out));
82. for(int i=1;i<=n;i++)//缩点统计度
83. {
84. for(int j=head[i];~j;j=e[j].next)
85. if(Scc[i]!=Scc[e[j].t])
86. in[Scc[e[j].t]]=out[Scc[i]]=1;
87. }
88. int a=0,b=0;
89. for(int i=1;i<=sig;i++)
90. {
91. if(in[i]==0)a++;
92. if(out[i]==0)b++;//没出度,需要加边
93. }
94. cout<<max(a,b)<<endl;
95. }
96. }
97. }
2、割点与割边(割顶&桥)(无向图)
割顶:若去掉一个点和与这个点相连的边后,图不再连通,则这个点是割顶。
基本思路:dfn数组记录时间戳,去更新low数组代表的可到达的祖先最低时间戳。
若满足low[v] >= dfn[u],说明u的子树中v点不能到达u的祖先,因此u点是割点。
桥:去掉这条边,图不再连通。
若满足low[v] > dfn[u],说明u的子树中v点不能到达u及其祖先,因此边<u,v>是割边
例题,uva 315: tarjan算法求割点
割点需要特判根节点是否是割点,方法是统计根节点在dfs时得到的子树数目child,若>1说明root是割点。
【代码uva315】:
1. #include<stdio.h>
2. #include<stdlib.h>
3. #include<string.h>
4. #include<iostream>
5. #include<algorithm>
6. using namespace std;
7. const int MAX=202020;
8. struct node{
9. int s,t,next;
10. }e[MAX];
11. int head[MAX],cnt;
12. void add(int u,int v)
13. {
14. e[cnt]=node{u,v,head[u]};
15. head[u]=cnt++;
16. }
17. int dfn[MAX];//每个节点的访问时间编号
18. int low[MAX];//每个点能到达的最小编号
19. bool iscut[MAX];//标记割点
20. void tardfs(int u,int &lay,int fa)
21. {
22. //到达此点的时间
23. int child=0;//子树数目
24. for(int i=head[u];~i;i=e[i].next)
25. {
26. int t=e[i].t;
27. if(t==fa)continue;
28. if(dfn[t]==0)
29. {
30. tardfs(t,lay,u);
31. low[u]=min(low[u],low[t]);
32. child++;
33.
34. if(u!=1&&low[t]>=dfn[u])//满足子树回不到祖先
35. iscut[u]=1;
36. }
37. else //祖先
38. //此处是dfn!不是low,因为low可能已被更新,就跨过了割点
39.
40. }
41. if(u==1&&child>1)iscut[1]=1;
42. }
43. int tarjan(int n)
44. {
45. int sig=0;//割点数
46. int lay=1;//时间戳
47. sizeof(iscut));
48. sizeof(dfn));//时间戳归0
49. for(int i=1;i<=n;i++)
50. //若无向图连通,只进入1一次就全部tarjan出来了
51. if(!dfn[i])tardfs(i,lay,-1);
52. }
53. for(int i=1;i<=n;i++)
54. if(iscut[i])sig++;
55. return sig;//返回割点数
56. }
57. int main()
58. {
59. int n,m,q,u,v;
60. while(cin>>n,n)
61. {
62. sizeof(head));
63. cnt=0;
64. while(scanf("%d",&u),u)
65. {
66. do{
67. "%d",&v);
68. add(u,v);
69. add(v,u);
70. while(getchar()!='\n');
71. }
72. cout<<tarjan(n)<<endl;
73. }
74. }
例题,uva796 求桥数并输出
题意是求出桥的数目并输出。
【代码uva796】
1. #include<stdio.h>
2. #include<stdlib.h>
3. #include<string.h>
4. #include<iostream>
5. #include<algorithm>
6. using namespace std;
7. const int MAX=202020;
8. struct node{
9. int s,t,next;
10. }e[MAX];
11. int head[MAX],cnt;
12. void add(int u,int v)
13. {
14. e[cnt]=node{u,v,head[u]};
15. head[u]=cnt++;
16. }
17. int dfn[MAX];//每个节点的访问时间编号
18. int low[MAX];//每个点能到达的最小编号
19. bool bri[MAX];//标记桥
20. node ans[MAX];
21. void tardfs(int u,int &lay,int fa)
22. {
23. //到达此点的时间
24. for(int i=head[u];~i;i=e[i].next)
25. {
26. int t=e[i].t;
27. if(t==fa)continue;
28. if(dfn[t]==0)
29. {
30. tardfs(t,lay,u);
31. low[u]=min(low[u],low[t]);
32. if(low[t]>dfn[u])//满足子树回不到祖先和自己
33. //标记桥
34. }
35. else //祖先
36. //此处是dfn!不是low,因为low可能已被更新,就跨过了割点
37. }
38. }
39. int tarjan(int n)
40. {
41. int bridge=0;//桥数
42. int lay=1;//时间戳
43. sizeof(bri));
44. sizeof(dfn));//时间戳归0
45. for(int i=1;i<=n;i++)
46. //若无向图连通,只进入1一次就全部tarjan出来了
47. if(!dfn[i])tardfs(i,lay,-1);
48. }
49. for(int i=0;i<cnt;i++)
50. if(bri[i])bridge++;
51. return bridge;//返回桥
52. }
53. bool cmp(node a,node b)
54. {
55. if(a.s==b.s)return a.t<b.t;
56. return a.s<b.s;
57. }
58. int main()
59. {
60. int n,m,q,u,v;
61. while(cin>>n)
62. {
63. sizeof(head));
64. cnt=0;
65. for(int i=1;i<=n;i++)
66. {
67. "%d (%d)",&u,&q);u++;
68. while(q--)
69. {
70. "%d",&v);v++;
71. add(u,v);
72. add(v,u);
73. }
74. }
75. "%d critical links\n",tarjan(n));//桥数
76. int t=0;
77. for(int i=0;i<cnt;i++)//对标记的边筛选排序并输出
78. {
79. if(bri[i])
80. {
81. if(e[i].s<e[i].t)
82. ans[t++]=node{e[i].s,e[i].t};
83. else ans[t++]=node{e[i].t,e[i].s};
84. }
85. }
86. sort(ans,ans+t,cmp);
87. for(int i=0;i<t;i++)
88. "%d - %d\n",ans[i].s-1,ans[i].t-1);
89. "");
90. }
91. }
92.