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.特殊代码解释

    对了这里要解释一条神奇的语句 

      low[cur] = min(low[cur],dfn[v]);
    为什么强联通的时候写成
      low[cur] = min(low[cur],low[v]);
    因为强联通的low承担的就只有一个任务,最早能回溯到的节点
    但是在割点中low还承担了一个任务,就是搜索子树的个数的任务
    
    看个神奇的例子就知道了
      

 

 

    我们直接从3号节点开始,此时low[3] = dfn[3] = 3;
    如果走了3 --> 1那么low[3] 更新为 low[1] = 1
    那么到5号节点的时候low[5] = dfn[5]更新3的low就会回到比3还早的1号节点,事实证明这是不可以的,因为父亲节点是中转点,如果没有了3,那么5就回不到1,
    所以在5的时候要写这条语句
      low[cur] = min(low[cur],dfn[v]);
 
  
  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.割边定义
    与割点的定义类似,一旦取消某条边之后,某些点就被分离了。
     比如说:
      

 

     里面的每一条边都是割边

    (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 }
posted @ 2022-02-16 14:13  scannerkk  阅读(123)  评论(0)    收藏  举报