使用Tarjan算法计算强连通分量

本文详细介绍了强连通分量的概念,并通过实例解释了Tarjan算法如何求解有向图的强连通分量。在Tarjan算法中,通过dfs序、low值和sccno值来判断节点是否属于同一个SCC,并进行了栈的运用来实现SCC的划分。此外,还探讨了缩点的概念,即将SCC视为单个节点简化图的过程。最后,给出了模板题的解题思路和代码实现,包括邻接表和链式前向星两种存储方式,以及如何优化时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

强连通分量

  • 强连通分量(Strongly Connected Component)简称SCC
  • 首先要理解一个定义,什么是强连通分量
  • 所谓强连通,有两个点 u u u v v v,从 u u u v v v有一条路径,从 v v v u u u也有一条路径,那么说这两个点之间是强连通的,也叫做互相可达;如果一个有向图 G G G中的任意两个点之间都是强连通的,那么就说这个图是一个强连通图
  • 强连通分量。如果一个有向图 G G G不是强连通图,那么可以把它分成多个子图,且每个子图的内部是强连通的,而且这些子图已经扩展到最大,不能与这个子图以外的任意点强连通,可称这个图是一个极大联通子图。那么这个极大联通子图就是图 G G G的一个强连通分量
  • 画一个图理解一下,见下图
    在这里插入图片描述
  • 上图中有四个SCC,分别是 { 1 , 2 , 3 , 4 } , { 5 } , { 6 } , { 7 } \{1,2,3,4\},\{5\},\{6\},\{7\} {1,2,3,4},{5},{6},{7}
  • 关于SCC可以容易的得到一个定理,就是在一个SCC中,从其中任何一个点出发,都至少有一条路径能绕回到自己,这个定理是显然的

Tarjan算法求解SCC

算法详解

  • 那么如何去求一个有向图中有多少个SCC呢?Tarjan提出了一种求解的办法
  • 设置三个数组, n u m [ i ] num[i] num[i]表示节点 i i i d f s dfs dfs序,也叫做时间戳; l o w [ i ] low[i] low[i]表示 i i i节点能够回溯到的最远祖先, s c c n o [ i ] sccno[i] sccno[i]表示 i i i节点位于第几个 S C C SCC SCC
  • 设置一个栈,栈上方的一些连续元素用来储存当前SCC的节点
    在这里插入图片描述
  • 还是用这个图做例子,我们先自己看看怎么求SCC,首先我们很容易的可以看出来 { 1 , 2 , 3 , 4 } \{1,2,3,4\} {1,2,3,4}属于一个SCC,那么我们就看它,怎么把它和图里其他SCC分开
  • 首先我们从1出发,进行 d f s dfs dfs搜索,显然搜索路径为1-3-4-2,把这四个元素都入栈,前面说了 n u m num num数组表示的是节点的时间戳(进入递归的顺序), l o w low low数组初始值和 n u m num num相同,那么显然,这四个节点的初始时间戳、回溯值和SCC标号如下
inum[i]low[i]sccno[i]
num[1]110
num[2]440
num[3]220
num[4]330
  • 现在的这些值都已经记录好了,到达节点2,接下来要到节点1了,这时候发现节点1已经进入递归了,也就是 n u m num num数组有标记,所以这时候1就不再进入递归,而是更新它的 l o w low low值为它能够回溯到的最早的祖先节点显然是它本身,更新方式为 l o w [ u ] = m i n ( l o w [ u ] , n u m [ v ] ) low[u]=min(low[u],num[v]) low[u]=min(low[u],num[v])
  • 上述公式改为 l o w [ u ] = m i n ( l o w [ u ] , l o w [ v ] ) low[u]=min(low[u],low[v]) low[u]=min(low[u],low[v])也不影响正确性,原因是到这一步更新必然是到达了祖先节点,而祖先节点的 l o w [ v ] = n u m [ v ] low[v]=num[v] low[v]=num[v]始终成立
  • 可以看出,第一个进入 d f s dfs dfs的节点就是它所在SCC的祖先节点
  • 接下来,需要进行的是更新这个SCC上面的所有节点的 l o w low low值,因为整个过程是递归进行的,所以这步操作是一个回溯的过程,写在递归后面即可,方程为 l o w [ u ] = m i n ( l o w [ u ] , l o w [ v ] ) low[u]=min(low[u],low[v]) low[u]=min(low[u],low[v])意思就是从后往前更新回溯值,这样一圈操作下来,得到这个SCC的各个节点的信息如下
inum[i]low[i]sccno[i]
num[1]110
num[2]410
num[3]210
num[4]310
  • 容易发现,只有祖先节点的 n u m num num l o w low low是相等的,所以如果发现某个节点在递归结束以后满足这个特性,那么就一定是祖先节点,这时候,将栈顶祖先节点之前(包括本身)的元素全部弹出,加上SCC标号,这样就成功的划分出了一个SCC
inum[i]low[i]sccno[i]
num[1]111
num[2]441
num[3]221
num[4]331
  • 可能有人会觉得这个例子过于简单,要是走到了别的SCC内部怎么办,这就牵扯到了一个关键话题,就是这个栈里面并不是只有一个SCC,它是根据 l o w low low n u m num num划分的不同SCC,也就是以 l o w [ u ] = = n u m [ u ] low[u]==num[u] low[u]==num[u]为一层,将栈划分成了若干个SCC
  • 还是上面这个例子,比如要是从5号节点开始 d f s dfs dfs会怎样呢?比如说它先到4,那么 d f s dfs dfs序为5-4-2-1-3-7-6,那么显然5在栈底,到3以后,接下来是4,发现4已经进入了递归,这时候更新3、1、2的 l o w low low值与4相同,结果还是 l o w [ 4 ] = n u m [ 4 ] low[4]=num[4] low[4]=num[4],这样把栈顶元素一直到4这一段弹出,标记为第一个SCC,然后现在栈里就一个5,继续遍历到7-6,接下来是3,发现3进入了递归且已经标好了SCC,所以3就和接下来没关系了,现在从栈顶到栈底依次为6、7、5,因为没有接下来的节点了,所以一看 l o w [ 6 ] = n u m [ 6 ] low[6]=num[6] low[6]=num[6],所以就弹出6,标记为第二个SCC,弹出7标记为第三个SCC,最后弹出5,标记为第四个SCC, t a r j a n tarjan tarjan算法结束
  • 代码实现如下
#include <iostream>
#include <stack>
#include <vector>
using namespace std;
const int MAXN = 1e6 + 100;
vector<int> g[MAXN];
int num[MAXN];
int sccno[MAXN];
int low[MAXN];
int dfn;
int cnt;
stack<int> st;
void tarjan(int s){
    st.push(s);
    low[s] = num[s] = ++cnt;
    for(int i=0;i<g[s].size();i++){
        int v = g[s][i];
        if(!num[v]){
            tarjan(v);
            low[s] = min(low[s], low[v]);
        }else if(!sccno[v]){
            low[s] = min(low[s], num[v]);
        }
    }
    if(low[s] == num[s]){
        cnt++;
        while(1){
            int v = st.top();
            st.pop();
            sccno[v] = cnt;
            if(v == s) break;
        }
    }
}
  • 这里我暂且使用邻接表存图,之后再使用链式前向星改写
  • 还有一个问题,图不连通怎么办,我们只需要把每个没有进入递归的节点都进行一次 t a r j a n tarjan tarjan算法即可

缩点

  • 经过上面的操作,我们已经将整个图划分为了若干个SCC,这样其实就起到了一个化简图的过程,每个SCC可以看作为一个点,这也就是我们所说的缩点,根据SCC标号,对于上述例子,我们可以构建一个新图如下
    在这里插入图片描述

  • 这就是缩点之后得到的图,可以发现,SCC之间的指向关系和之前的关系是一致的,这是显然成立的,我们利用这一点来建图

  • 这时候我们可以仔细观察一下SCC的标号,这个标号实际上是一个逆拓扑序,这个性质在后面的 2 − S A T 2-SAT 2SAT问题中有应用

练习

模板题

  • 这个题思路如下:首先我们要建图,接着使用 t a r j a n tarjan tarjan算法进行缩点,同时记录每一个SCC的内部权值和,之后要重新建图,最后对于每一个SCC计算它能够达到的最大点权和,取所有的最大值即为答案
#include <iostream>
#include <stack>
#include <cstring>
#include <vector>
using namespace std;
const int MAXN = 2e5 + 100;
vector<int> g[MAXN];
int num[MAXN];
int sum[MAXN];
int low[MAXN];
int sccno[MAXN];
int val[MAXN];
int x[MAXN];
int y[MAXN];
int f[MAXN];
int dfn;
int cnt;
inline int read(){
	int x = 0, f = 1;char c = getchar();
	while(c < '0' || c > '9'){if(c == '-') f = -1;c = getchar();}
	while(c >= '0' && c <= '9'){x = (x << 1) + (x << 3) + (c ^ 48);c = getchar();}
	return x * f;
}
stack<int> st;
void tarjan(int u){
	low[u] = num[u] = ++dfn;
	st.push(u);
	for(int i=0;i<g[u].size();i++){
		int v = g[u][i];
		if(!num[v]){
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}else if(!sccno[v]){
			low[u] = min(low[u], num[v]);
		}
	}
	if(low[u] == num[u]){
		cnt++;
		while(1){
			int v = st.top();
			st.pop();
			sccno[v] = cnt;
			sum[cnt] += val[v];
			if(u == v) break;
		}
	}
}
void dfs(int u){
	if(f[u]) return;
	f[u] = sum[u];
	int maxsum = 0;
	for(int i=0;i<g[u].size();i++){
		int v = g[u][i];
		if(!f[v]){
			dfs(v);
		}
		maxsum = max(maxsum, f[v]);
	}
	f[u] += maxsum;
}
int main(){
	int n, m;
	n = read();
	m = read();
	for(int i=1;i<=n;i++) val[i] = read();
	for(int i=0;i<m;i++){
		x[i] = read();
		y[i] = read();
		g[x[i]].push_back(y[i]);
	}
	for(int i=1;i<=n;i++){
		if(!num[i]){
			tarjan(i);
		}
	}
	memset(g, 0, sizeof g);
	for(int i=0;i<m;i++){
		if(sccno[x[i]] != sccno[y[i]]){
			g[sccno[x[i]]].push_back(sccno[y[i]]);
		}
	}
	int ans = -1;
	for(int i=1;i<=cnt;i++){
		if(!f[i]){
			dfs(i);
		}
		ans = max(ans, f[i]);
	}
	cout << ans;
	return 0;
}
  • 修改为链式前向星
#include <iostream>
#include <stack>
#include <cstring>
#include <vector>
using namespace std;
const int MAXN = 2e5 + 100;
int num[MAXN];
int sum[MAXN];
int low[MAXN];
int sccno[MAXN];
int val[MAXN];
int x[MAXN];
int y[MAXN];
int f[MAXN];
int head[MAXN];
int dfn;
int cnt;
int tot;
struct Edge{
	int to;
	int next;
	int val;
}edge[MAXN];
inline int read(){
	int x = 0, f = 1;char c = getchar();
	while(c < '0' || c > '9'){if(c == '-') f = -1;c = getchar();}
	while(c >= '0' && c <= '9'){x = (x << 1) + (x << 3) + (c ^ 48);c = getchar();}
	return x * f;
}
void Add_Edge(int u, int v, int w){
	edge[cnt].to = v;
	edge[cnt].next = head[u];
	edge[cnt].val = w;
	head[u] = cnt++;
}
stack<int> st;
void tarjan(int u){
	low[u] = num[u] = ++dfn;
	st.push(u);
	for(int i=head[u];i!=-1;i=edge[i].next){
		int v = edge[i].to;
		if(!num[v]){
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}else if(!sccno[v]){
			low[u] = min(low[u], num[v]);
		}
	}
	if(low[u] == num[u]){
		tot++;
		while(1){
			int v = st.top();
			st.pop();
			sccno[v] = tot;
			sum[tot] += val[v];
			if(u == v) break;
		}
	}
}
void dfs(int u){
	if(f[u]) return;
	f[u] = sum[u];
	int maxsum = 0;
	for(int i=head[u];i!=-1;i=edge[i].next){
		int v = edge[i].to;
		if(!f[v]){
			dfs(v);
		}
		maxsum = max(maxsum, f[v]);
	}
	f[u] += maxsum;
}
int main(){
	int n, m;
	n = read();
	m = read();
	memset(head, -1, sizeof head);
	for(int i=1;i<=n;i++) val[i] = read();
	for(int i=0;i<m;i++){
		x[i] = read();
		y[i] = read();
		Add_Edge(x[i], y[i], 1);
	}
	for(int i=1;i<=n;i++){
		if(!num[i]){
			tarjan(i);
		}
	}
	memset(head, -1, sizeof head);
	cnt = 0;
	for(int i=0;i<m;i++){
		if(sccno[x[i]] != sccno[y[i]]){
			Add_Edge(sccno[x[i]], sccno[y[i]], 1);
		}
	}
	int ans = -1;
	for(int i=1;i<=tot;i++){
		if(!f[i]){
			dfs(i);
		}
		ans = max(ans, f[i]);
	}
	cout << ans;
	return 0;
}
  • 时间优化幅度非常巨大
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Clarence Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值