Tarjan算法
一、Tarjan算法
与其说Tarjan是一种算法,不如说Tarjan是一种思想,利用这种思想我们可以求强联通分量(scc)、割点/边、缩点等问题,接下来我们就来说一下Tarjan是怎么解决以下几个问题的。
二、SCC
1.什么叫SCC?
定义就是在一个图中,如果任意两个点能够互相达到,那么就称几个点为SCC,一个点也可以看作SCC。
请看下面这幅图

1 2 3 4 两点之间可以相互到达,因此是一个SCC
5不能和任何一个点相互到达,但是也是一个SCC
6也是一个SCC
故这个图有3个SCC
2.那Tarjan的思路是什么呢?
我们以第一个点为根节点进行第一次dfs遍历
(1)首先从1开始到3
(2)然后从3到5
(3)再从5到6,发现没地方走了,就返回
(4)然后看5,5也没地方走了就返回
(5)回到3,还能走4这条边
(6)4能走到1
(7)因为1已经访问过了,就不能访问了,所以只能返回了
(8)3没路走了就返回了
(9)1还有2没走过,就走2
(10)2能走4,但是4也走过了,所以只能返回
至此这个图就遍历完成了,那怎么知道SCC呢?
3.tarjan的运行
为了知道SCC我们需要引进两个数组,以及一个变量,在dfs的过程中记录某些信息
两个数组分别是:dfn、low
一个变量是:idx
解释一下dfn数组的含义,就是dfs第几次到该点的(给它一个定义叫作时间戳)
low数组的含义,就是你最早能回溯到的时间戳的位置
idx标记时间戳
怎么运行的呢?
初始化1的dfn和low为1,因为1是第一个被遍历到的嘛
(1)首先从1开始到3,标记3的dfn和low为2
(2)然后从3到5,标记5的dfn和low为3
(3)再从5到6,标记6的dfn和low为4,发现没地方走了,就返回
(4)然后看5,5也没地方走了就返回
(5)回到3,还能走4这条边,标记4的dfn和low为5
(6)4能走到1,更新4的low为1的dfn
(7)因为1已经访问过了,就不能访问了,所以只能返回了
(8)回溯到3,把3的也要更新,因为3能通过4到1嘛,3没路走了就返回了,
(9)1还有2没走过,就走2,标记2的dfn和low为6
(10)2能走4,更新2的low为4的low,也就是1,但是4也走过了,所以只能返回
至此就已经遍历完成了,细心的同学就会发现凡是low相等的,都是1个SCC里面的。知道了这个性质我们就需要想一个问题,怎么存下来呢?
4.存储SCC
我们利用一个栈去存储每一次遍历的点,为什么用栈呢?因为dfs的过程就是入栈的过程,我们采用这个数据结构存的值一定是正确的。
每一次遍历到一个点的时候我们就存入栈,那什么时候出栈呢?当我们的dfn和low相等的时候,说明这个点不能去返回他的父亲节点了,那么这个点一定就是根节点,那么把他后面入栈的全部pop 掉,就可以得到关于这个点的scc变量了。
比如说访问到5了,发现可以到6,我再访问6,6不能去其他点了,要回溯了,此时dfn等于low说明6是一个SCC
返回到5,此时5的low等于dfn说明5也是一个SCC
5.代码实现
1 #include "bits/stdc++.h" 2 using namespace std; 3 struct node{ 4 int v; 5 int nxt; 6 }edges[2010]; 7 int vis[2110],dfn[2110],low[2110];//dfn表示第几个被dfs到,vis表示该点是否在栈中,表示已经搜过了,low表示最早能够回溯到的位置 8 int heads[2110]; 9 int cnt,idx; 10 int n,m,ans; 11 stack <int> scc; 12 void init() 13 { 14 for(int i = 1;i <= n;i++) 15 heads[i] = -1; 16 } 17 void tarjan(int x) 18 { 19 low[x] = dfn[x] = ++idx; 20 vis[x] = 1; 21 scc.push(x); 22 for(int i = heads[x]; i != -1;i = edges[i].nxt){ 23 //如果这个点没有搜过,那就搜下去 24 if(!dfn[edges[i].v]){ 25 tarjan(edges[i].v); 26 //更新最早能够回溯到的位置 27 low[x] = min(low[x],low[edges[i].v]); 28 } 29 //如果这个边已经被访问过啦,就是说防止从儿子边访问父亲边 30 else if(vis[edges[i].v]) 31 //注意这里dfn[edges[i].v],因为这样子写就不会把其他的节点牵扯进来,dfn是没有改变的,但是low[edges[i].v]可能会被其他强联通连进去,因此求割点时不能比较low[x] 和 low[edges[i].v] 32 low[x] = min(low[x],dfn[edges[i].v]); 33 } 34 //强联通分量 35 if(dfn[x] == low[x]){ 36 int v; 37 do { 38 v = scc.top(); 39 scc.pop(); 40 cout << v << ' ';//打印SCC 41 vis[v] = false;//为什么要标记为false呢,因为我可能还会从其他地方访问这个点。 42 idx--; 43 }while(v != x); 44 cout << '\n'; 45 } 46 } 47 void add(int x,int y) 48 { 49 edges[++cnt].nxt = heads[x]; 50 edges[cnt].v = y; 51 heads[x] = cnt; 52 } 53 int main() 54 { 55 //输入顶点个数和边条数 56 cin >> n >> m; 57 //初始化heads数组 58 init(); 59 //链式前向星存储图(就是邻接表) 60 for(int i = 1;i <= m;i++){ 61 int u,v; 62 cin >> u >> v; 63 add(u,v); 64 } 65 for(int i = 1;i <= n;i++) 66 //可能存在孤立点也是强联通分量,也就是图可能不连通所以每个点都要遍历一遍 67 if(!dfn[i]) 68 tarjan(i); 69 return 0; 70 }
三、缩点
所谓缩点,就是把那几个SCC全部当成一个点看,然后看作一个新的图(注意此时这个图一定是一个有向无环图(DAG),因为有环的都被拿去当SCC了嘛)
那我们的工作就是把这几个SCC重新建一个新的图,使他成为一个DAG, 那么我们可以引进一个color数组,表示哪几个是同一个scc,通过这种方式再去遍历原来的图,去构造一个新的图就很简单了。
比如说这个图吧

我们把1、2、3、4看作一个点
5看作一个点
6看作一个点
那么图就会变成这样嘛

是不是一个DAG?
这里比较简单,就不分析了,直接上代码
1 #include "bits/stdc++.h" 2 using namespace std; 3 int dfn[1100],low[1100]; 4 int heads[1010]; 5 //边集数组 6 struct node{ 7 int from; 8 int to; 9 int nxt; 10 }edges[10010]; 11 //记录当前正在访问的点,也就是存储强联通分量 12 stack <int> que; 13 //标记哪几个顶点是一个强联通分量的 14 int color[1100]; 15 //标记顶点是否在访问中 16 bool vis[1100]; 17 //统计重新建的图的点的入度和出度 18 int out[1100],in[1100]; 19 //染色,也就是标记强联通分量 20 int num; 21 int n,m; 22 int cnt; 23 int idx; 24 //初始化heads数组 25 void init() 26 { 27 for(int i = 1;i <= n;i++) 28 heads[i] = -1; 29 } 30 //邻接表存信息 31 void add(int u,int v) 32 { 33 edges[++cnt].from = u; 34 edges[cnt].to = v; 35 edges[cnt].nxt = heads[u]; 36 heads[u] = cnt; 37 } 38 //tarjan算法 39 void tarjan(int cur) 40 { 41 low[cur] = dfn[cur] = ++idx; 42 que.push(cur); 43 vis[cur] = true; 44 for(int i = heads[cur];i != -1;i = edges[i].nxt){ 45 int v = edges[i].to; 46 if(!dfn[v]){ 47 tarjan(v); 48 low[cur] = min(low[cur],low[v]); 49 } 50 else if(vis[v]) 51 low[cur] = min(low[cur],dfn[v]); 52 } 53 //统计强联通分量 54 if(dfn[cur] == low[cur]){ 55 num++; 56 int vertex; 57 do{ 58 vertex = que.top(); 59 que.pop(); 60 color[vertex] = num; 61 vis[vertex] = false; 62 idx--; 63 }while(vertex != cur); 64 } 65 } 66 int main() 67 { 68 cin >> n >> m; 69 init(); 70 for(int i = 1;i <= m;i++){ 71 int u,v; 72 cin >> u >> v; 73 add(u,v); 74 } 75 for(int i = 1;i <= n;i++) 76 if(!dfn[i]) 77 tarjan(i); 78 //打印染色情况 79 for(int i = 1;i <= n;i++) 80 cout << color[i] << endl; 81 //重新建图 82 for(int i = 1;i <= m;i++){ 83 int sx,sy; 84 sx = color[edges[i].from]; 85 sy = color[edges[i].to]; 86 if(sx != sy){ 87 in[sy]++; 88 out[sx]++; 89 } 90 } 91 for(int i = 1;i <= n;i++){ 92 cout << i << ':' << '\n'; 93 cout << "in:" << in[i] << '\n'; 94 cout << "out" << out[i] << '\n'; 95 } 96 //test case: 97 // 6 8 98 // 1 2 99 // 1 3 100 // 2 4 101 // 3 4 102 // 4 6 103 // 3 5 104 // 5 6 105 // 4 1 106 107 //answer 108 // 1 2 3 4 109 // 5 110 // 6 111 112 113 //此时1234标记为3,5标记为2,6标记为1,可以看出答案是正确的 114 // 1: 115 // in:2 116 // out0 117 // 2: 118 // in:1 119 // out1 120 // 3: 121 // in:0 122 // out2 123 // 4: 124 // in:0 125 // out0 126 // 5: 127 // in:0 128 // out0 129 // 6: 130 // in:0 131 // out0 132 return 0; 133 }
四、割点(主要出现在于无向图)
1.割点定义(也叫作割顶)
第一种情况
就是说如果这个点去掉,那么这个点的儿子无法通过其他点回到这个点的父亲节点,那么这个点就叫做割点
看图

此时如果3去掉,1和2,4和5就会是两个独立的子树,我们称3为割点
第二种情况
如果某个节点的孩子个数大于等于2,这个节点也是割点
看图

此时3就是割点
那么有些同学就要说了,那这张图呢?

此时这个3就不能算有两个孩子了,因为在dfs中3能走到4,然后从4到5,很明显5的父亲就是4,不是3的孩子
这两种情况转化成代码就是
if(root != cur && low[next] >= dfn[cur])
cur就是割点
if(root == cur && child >= 2)
cur就是割点
其他的就和上面的代码差不多了
2.特殊代码解释
对了这里要解释一条神奇的语句
我们直接从3号节点开始,此时low[3] = dfn[3] = 3;
1 #include "bits/stdc++.h" 2 using namespace std; 3 const int N = 2e4; 4 bool cut[N + 10]; 5 int dfn[N + 10],low[N + 10]; 6 int idx; 7 int n,m; 8 int heads[N + 10]; 9 struct node{ 10 int to; 11 int nxt; 12 }edges[200010]; 13 int cnt; 14 void add(int u,int v) 15 { 16 edges[++cnt].to = v; 17 edges[cnt].nxt = heads[u]; 18 heads[u] = cnt; 19 } 20 void init() 21 { 22 for(int i = 1;i <= N + 10;i++) 23 heads[i] = -1; 24 } 25 void tarjan(int cur,int fa,int root) 26 { 27 int v; 28 int child = 0; 29 dfn[cur] = low[cur] = ++idx; 30 for(int i = heads[cur];i != -1;i = edges[i].nxt){ 31 v = edges[i].to; 32 if(!dfn[v]){ 33 child++; 34 tarjan(v,cur,root); 35 low[cur] = min(low[cur],low[v]); 36 if(cur == root && child >= 2) 37 cut[cur] = true; 38 if(cur != root && low[v] >= dfn[cur]) 39 cut[cur] = true; 40 } 41 //防止从儿子访问父亲 42 else if(v != fa) 43 low[cur] = min(low[cur],dfn[v]); 44 } 45 } 46 int main() 47 { 48 cin >> n >> m; 49 init(); 50 for(int i = 1;i <= m;i++){ 51 int u,v; 52 cin >> u >> v; 53 add(u,v); 54 add(v,u); 55 } 56 for(int i = 1;i <= n;i++) 57 if(!dfn[i]) 58 tarjan(i,i,i); 59 int ans = 0; 60 for(int i = 1;i <= n;i++) 61 ans += cut[i]; 62 cout << ans << endl; 63 for(int i = 1;i <= n;i++) 64 if(cut[i]) 65 cout << i << ' '; 66 return 0; 67 }
里面的每一条边都是割边
(1)去掉4-5这条边,那么5就被孤立了
(2)去掉3-4这条边,4和5就被孤立了
(3)去掉2-3这条边,那么3 4 5就被孤立了
(4)去掉1-2这条边,那么2 3 4 5就被孤立了
所以有四条割边
2.割边实现
我们发现被取消的那条边,都是连父亲节点都回不去了,因此和割点代码类似
转换成代码为if(low[next] > dfn[cur])
此时cur---->next就是割边
只有这一种情况。所以比割点代码还要简单一点
3.代码实现
1 #include "bits/stdc++.h" 2 using namespace std; 3 int n,m; 4 int dfn[100010]; 5 int low[100010]; 6 int heads[100010]; 7 struct node{ 8 int to; 9 int nxt; 10 }edges[200010]; 11 int ans; 12 int cnt; 13 int idx; 14 void init() 15 { 16 for(int i = 1;i <= n;i++) 17 heads[i] = -1; 18 } 19 void add(int u,int v) 20 { 21 edges[++cnt].to = v; 22 edges[cnt].nxt = heads[u]; 23 heads[u] = cnt; 24 } 25 void tarjan(int cur,int fa) 26 { 27 dfn[cur] = low[cur] = ++idx; 28 for(int i = heads[cur];i != -1;i = edges[i].nxt){ 29 int v = edges[i].to; 30 if(!dfn[v]){ 31 tarjan(v,cur); 32 low[cur] = min(low[cur],low[v]); 33 if(low[v] > dfn[cur]) 34 cout << cur << "->" << v << endl; 35 } 36 else if(v != fa) 37 low[cur] = min(low[cur],dfn[v]); 38 } 39 } 40 int main() 41 { 42 cin >> n >> m; 43 init(); 44 int u,v; 45 for(int i = 1;i <= m;i++){ 46 cin >> u >> v; 47 add(u,v); 48 add(v,u); 49 } 50 for(int i = 1;i <= n;i++) 51 if(!dfn[i]) 52 tarjan(i,i); 53 return 0; 54 }
本文来自博客园,作者:{scanner},转载请注明原文链接:{https://home.cnblogs.com/u/scannerkk/}

浙公网安备 33010602011771号