强连通分量(Tarjan算法) 图解

强连通分量(Tarjan算法)

  • 前言

    第一件事:没事不要while(m–),会带来不幸
    第二件事:看博客先看看评论,如果博主他写错了的话…

  • 简介

    先讲几个定义

    • 强连通:两个顶点 u u u, v v v 可以相互到达

    • 强连通图:图中的任意两个顶点可以相互到达(就是图中有一块边缠在一起)

    • 强连通分量(scc):图 G G G 不是一个强连通图,但其子图 G ′ G^\prime G′ 是 G G G 的最大强连通子图,则称 G ′ G^\prime G′ 为一个强连通分量

    tarjan算法就是一个可以找出图中所有强连通分量的方法。

  • 原理

    维护两个数组:
    dfn[] : 时间戳。深搜这个图,dfn[i] 表示 第 i 个点是什么时候被搜到的
    low[] : 深搜的这个图里正常的话肯定有环,low[i] 就表示第 i 个点属于哪个时间戳节点的环里
    (表达很抽象自己悟233,或者看后面图解过程)
    同时手打一个栈,把搜过的点压进栈

强连通分量(Tarjan算法) 图解

如图,从点1开始深搜,给搜过的点打上时间戳dfn,初始化low,stack = 1 3 2

强连通分量(Tarjan算法) 图解

当搜到点2时,下一个搜到了1,1已经被搜过了(已经被搜过了看 dfn[] != 0 ),且1在栈内
修改 low[2] = min(low[2], dfn[1]); (注意绕回根节点的时候是将该点的low和根节点的dfn比较)

强连通分量(Tarjan算法) 图解

此时点2搜完了,回溯到点3. 点3是回溯来的修改low肯定是和点2的low比较
即修改 low[3] = min(low[2], low[3]);

强连通分量(Tarjan算法) 图解

点3继续深搜,给4,5打上时间戳,同时入栈4,5,此时栈内有 stack = 1 3 2 4 5

强连通分量(Tarjan算法) 图解

5继续搜,搜到4,点4已经搜过且在栈内,修改low[5] = min(low[5], dfn[4])
回溯到点4,点4修改low[4] = min(low[4], low[5])

强连通分量(Tarjan算法) 图解

下一步是关键,此时点4已经搜完,即将退出回溯到点3,而我们发现点4的dfn[4] == low[4]
也就是说点4就是一块强连通分量的节点!
此时开始出栈,直到点4出栈为止。即剩下的stack = 1 3 2
我们给出栈的点全部打上scc标记,即

强连通分量(Tarjan算法) 图解

待出栈结束,回溯到点3
点3此时修改low[3] = min(low[3], low[4]), 显然low3不变
继续回溯到点1,发现点1已经搜完,且dfn[1] == low[1]
同上述点4,出栈打标记

强连通分量(Tarjan算法) 图解

此时栈已经空了,从1开始深搜已经结束,但很明显还有一个6没有被搜过
可以很明显看出上述算法不一定能走遍整个图,所以需要以每一个点为起点深搜一次,判断标准是是否被搜过(即dfn[] != 0)

for(int i = 1; i <= n; i++){
	if(!dfn[i]) tarjan(i);
}

在坚持一下,马上就要结束了
此时其实还有最后问题没有解决,就是那个栈有什么用,让我们继续深搜点6
dfn[6] = 6, low[6] = 6,下一个要搜的点是2
2已经被搜过,但是和上面都不一样的地方来了!2此时不在栈里
所以我们不用更新low[6],直接结束搜点6.

最后,low[6] == dfn[6], 打上scc标记,整个算法就讲完了。

强连通分量(Tarjan算法) 图解

  • 代码

    用链式前状星存图,手打栈(当然stl也可以)

    void tarjan(int x) {
        sta[++top] = x;	//手打入栈
        instack[x] = 1;	
        
        dfn[x] = low[x] = ++cnt; //初始化dfn和low
        
        //核心代码,深搜这个点
        for (int i = head[x]; i; i = edge[i].next) {
            int to = edge[i].to;
            if(!dfn[to]){	//如果下一个点没有被搜过,
                tarjan(to);	//继续深搜
                low[x] = min(low[x], low[to]); //更新low
            }
            else if(instack[to]){ //如果下一个点已经被搜过,看他在不在栈里
                low[x] = min(low[x], dfn[to]); //更新low
            }
        }
        //如果dfn==low,即找完了一整块强连通分量,出栈+标记
        if(dfn[x] == low[x]){
            tot++;//计数器++,第几个强连通分量
            while(sta[top+1]!=x){ 		//不加1的话x这个点不会被出栈
                scc[sta[top]] = tot;	//打标记
                instack[sta[top--]] = 0;//出栈
            }
        }
    }
    
  • 例题:P3387 【模板】缩点

    #define _CRT_SECURE_NO_WARNINGS
    #include <bits/stdc++.h>
    using namespace std;
    #define ll long long
    
    const int N = 1e4 + 5;
    const int M = 1e5 + 5;
    int read() {
        int x = 0, f = 1; char ch = getchar();
        while (ch < '0' || ch>'9') { if (ch == '-') f = -1; ch = getchar(); }
        while (ch >= '0' && ch <= '9') { x = x * 10 + ch - 48; ch = getchar(); }
        return x * f;
    }
    ///
    struct Edge {
        int to;
        int from;
        int next;
    }edge[M], edge2[M];
    int head[N], head2[N];
    int cnt;
    void add_edge(int u, int v) {
        edge[++cnt].to = v;
        edge[cnt].from = u;
        edge[cnt].next = head[u];
        head[u] = cnt;
    }
    void add_edge2(int u, int v) {
        edge2[++cnt].to = v;
        edge2[cnt].from = u;
        edge2[cnt].next = head2[u];
        head2[u] = cnt;
    }
    
    int w[N];
    int low[N];
    int dfn[N];
    int scc[N];
    int sum[N];
    int tot;//计数有几个强连通分量
    
    //手打栈
    bool instack[N];
    int sta[N];
    int top;
    
    void tarjan(int x) {
        sta[++top] = x;
        instack[x] = 1;
        dfn[x] = low[x] = ++cnt;
        for (int i = head[x]; i; i = edge[i].next) {
            int to = edge[i].to;
            if(!dfn[to]){
                tarjan(to);
                low[x] = min(low[x], low[to]);   
            }
            else if(instack[to]){
                low[x] = min(low[x], dfn[to]);
            }
        }
        if(dfn[x] == low[x]){
            tot++;
            while(sta[top+1]!=x){
                scc[sta[top]] = tot;
                sum[tot] += w[sta[top]];
                instack[sta[top--]] = 0;
            }
        }
    }
    
    
    int dp[N];
    void dfs(int x) {
        if (dp[x]) return;
        dp[x] = sum[x];
        int sum = 0;
        for (int i = head2[x]; i; i = edge2[i].next) {
            int to = edge2[i].to;
            if (!dp[to]) 
                dfs(to);
            sum = max(sum, dp[to]);
        }
        dp[x] += sum;
    }
    
    int main()
    {
        int n, m;
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; i++) {
            w[i] = read();
        }
    
        for(int i = 0; i < m; i++){
            int a = read();
            int b = read();
            add_edge(a, b);
        }
        cnt = 0;//偷懒给tarjan复用
    
        for (int i = 1; i <= n; i++) {
            if (!dfn[i]) 
                tarjan(i);
        }
    
        //建新的图edge2,新图是DAG有向无环图
        cnt = 0;//偷懒x2
        for (int i = 1; i <= m; i++) {
            int u = edge[i].from;
            int v = edge[i].to;
            if(scc[u] != scc[v])
                add_edge2(scc[u], scc[v]);
        }
    
        //深搜这个edge2
        int ans = 0;
        for (int i = 1; i <= tot; i++) {
            if (!dp[i]) {
                dfs(i);
                ans = max(ans, dp[i]);
            }
        }
        
        printf("%d\n", ans);
        system("pause");
        return 0;
    }
    
  • 尾声

    感觉这个应该是最详细的tarjan博客了23333
    最后提醒两点:看博客注意看评论会有大牛指出错误,不要对着错博客研究半天怎么都写不对2333
    第二个,上面的代码94行画图的时候,老老实实for(int i = 0;i < m; i++),别嫌那么一点点麻烦来个while(m--),108行第二次画图的时候就啥都没干了

上一篇:逆序数的实现——基于二路归并排序


下一篇:【LeetCode - 1055】形成字符串的最短路径