[算法鉴赏][图论]强连通分量问题---Tarjan算法

本文涉及十分多的概念,同时也有精彩的算法Tarjan,为了方便理解,本文 的一部分gif来源于其他博客,特此说明。

一,相关概念
1.联通:在无向图中,如果存在一条路径从 \(u\)\(v\),那么称 \(u,v\) 是联通的
2.联通图:如果在无向图中选取任意一对顶点都是联通的,那么称这个图是连通图
3.联通分量:如果一个无向图不是联通的,但是其中的一个极大子图是联通的,那么就称那个极大子图是联通分量(极大指节点个数尽可能多)
4.强连通分量(Strongly Connected Components,SCC):在有向图中,一个联通分量中的任意两点都联通,也称为css
5.弱联通分量:在有向图中,忽略边方向的情况下是连通的

二,Tarjan算法
1.dfs生成树
由于tarjan算法是基于搜索树展开的,所以先来了解dfs生成树
当我们在一个有向图中,对每一个未访问的节点进行dfs操作,发现新点时,就加入生成树,最终新生成的树,称为dfs生成树,并且里面会包含所有原图中的点
由于边具有方向性,以及dfs的搜索特性,会出现许许多多有奇妙性质的边,大致可以分为四类:

树边(tree edge):示意图中以黑色边表示,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边.所有相邻的树边组成 DFS 生成树.
反祖边(back edge):也称回边,示意图中以红色边表示(即 \(7 \rightarrow 1\))指在搜索过程中,从某个结点指向其祖先结点的非树边.
前向边(forward edge):示意图中以绿色边表示(即 \(3 \rightarrow 6\))指在搜索过程中,从某个结点指向其子树中后代结点的非树边.
横叉边(cross edge):示意图中以蓝色边表示(即 \(9 \rightarrow 7\))指在搜索过程中,从某个结点指向非祖先、非后代且已访问的结点的边,即不属于上述三类的边.

2.Tarjan实现
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
Tarjan在dfs中维护了以下信息:
\(dfn_i\):dfs到 \(i\) 第一次的次序,也被称为时间戳
\(low_i\):为 \(i\)\(i\) 的子树能够在栈中追溯的最早的栈中节点的次序号(即最小的 \(dfn\) 值)(实际操作中 \(low_i\) 不一定最小,但不会影响程序的最终结果)(另外一种关于 \(low_u\) 的表述:记录该点所在的强连通子图所在搜索子树的根节点的 \(dfn\) 值)
一个栈 \(st\):当前搜索路径上且尚未归属SCC的顶点(用处在下方会说)

其中也有以下性质:
1.一个结点的子树内结点的 \(dfn\) 都大于该结点的 \(dfn\).(因为dfs一定先到根)
2.从根开始的一条路径上的 \(dfn\) 严格递增,\(low\) 严格非降.(追溯到的节点一定已经访问过,所以 \(low\) 一定会比现在的小)

当通过 \(u\) 搜索到一个新的节点 \(v\) 的时候可以有多种情况:
1、结点 \(u\) 通过树边到达结点 \(v\)
\(low_u = \min ( low_u, low_v)\)
2、结点 \(u\) 通过反祖边到达结点 \(v\) ,或者通过横叉边到达结点 \(v\) 并且满足 \(low\) 定义中 \(v\) 的性质
\(low_u = \min ( low_u, low_v)\)

认真思考,容易发现节点 \(u\) 是某个强连通分量等价于 \(dfn_u = low_u\);
所以在发现 \(dfn_u = low_u\) 时,我们只需要把栈中尚未归属强连通分量的顶点归属至 \(u\),当栈中的栈顶元素 \(k\) 不满足 \(low_k = dfn_u\) 时,说明栈中所以属于以 \(u\) 为根的强连通分量已经全部找到

所以tarjan的代码如下:

vector<int>pic[maxn];
int dfn[maxn],low[maxn],css[maxn];
stack<int>st;
bool vst[maxn];
void tarjan(int p)
{
	dfn[p]=low[p]=++cnt;
	st.push(p);vst[p]=1;
	for(auto i:pic[p]){
		if(dfn[i]==0){
			tarjan(i);
			low[p]=min(low[i],low[p]);
		}
		else if(dfn[i]!=0&&vst[i]){
			low[p]=min(low[p],dfn[i]);
		}
	}
	if(dfn[p]==low[p])
	{
		w[p]=0;
		while(1)
		{
			int t=st.top();
			st.pop();
			vst[t]=0;
			sccn[t]=p;
			if(t==p)break;
		}
	}
}

附上一个gif(来自csxx601cjy 十分感谢!)

这张动图演示了 Tarjan 算法的流程,其中黑色数字是结点编号(为了方便,dfn 和点权也是同样的数字),红色数字是 low,被彩色笔圈起来表示出栈了,不同颜色代表不同强连通分量。这个例子中,一共有三个强连通分量

3.例题与鉴赏
P3387 【模板】缩点 / 强连通分量
题意十分简明,尤为需要注意的是重复经过的点,权值只计算一次。
想想什么时候会出现重复经过一个点(注意是有向图),就是强连通分量中对吧
所以我们要用到一个叫做缩点的技巧:简单来说,就是把一个强连通分量当作一个点来看,运用拓扑排序(因为每一个强连通分量都用有向边连接)建图,我们只要求新图的最大点权之和
除了要维护tarjan相关变量以外,需要多维护一个邻接表用来建缩点后的图,一个数组 \(w\) 来维护一个强连通分量缩点后的权值,一个 \(dp\) 数组计算最大的点权之和
转移式也很好想:对于 \(u \rightarrow v\) 的边,\(dp_v = \max(dp_v, dp_u + w_v)\)

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define dep(i,a,b) for(int i=(a);i>=(b);i--)
#define ll long long
using namespace std;
const int maxn=1e5+7;
const int inf=0x3f3f3f3f;
int n,m,cnt=0,in[maxn];
vector<int>pic[maxn],pic2[maxn];
ll a[maxn],u[maxn],v[maxn],w[maxn];
int dfn[maxn],low[maxn];
ll dp[maxn],sccn[maxn];
stack<int>st;
bool vst[maxn];
void tarjan(int p)
{
	dfn[p]=low[p]=++cnt;
	st.push(p);vst[p]=1;
	for(auto i:pic[p]){
		if(dfn[i]==0){
			tarjan(i);
			low[p]=min(low[i],low[p]);
		}
		else if(dfn[i]!=0&&vst[i]){
			low[p]=min(low[p],dfn[i]);
		}
	}
	if(dfn[p]==low[p])
	{
		w[p]=0;
		while(1&&!st.empty())
		{
			int t=st.top();
			st.pop();
			vst[t]=0;
			sccn[t]=p;
			w[p]+=a[t];
			if(t==p)break;
		}
	}
}
void topo(){
	queue<int>q;
	rep(i,1,n) dp[i] = -inf;
	rep(i,1,n){
		if(i==sccn[i]&&in[i]==0){
			q.push(i);
			dp[i]=w[i];
		}
	}
	while(!q.empty())
	{
		int s=q.front();q.pop();
		for(auto i:pic2[s]){
			dp[i]=max(dp[i],dp[s]+w[i]);
			if(--in[i]==0)q.push(i);
		}
	}
}
int main()
{
	cin>>n>>m;
	rep(i,1,n)cin>>a[i];
	rep(i,1,m)
	{
		cin>>u[i]>>v[i];
		pic[u[i]].push_back(v[i]);
	}
	rep(i,1,n)if(dfn[i]==0)tarjan(i);
	rep(i,1,m)
	{
		int x=sccn[u[i]],y=sccn[v[i]];
		if(x!=y){
			pic2[x].push_back(y);
			in[y]++;
		}
	}
	topo();
	ll ans=-inf;
	rep(i,1,n)ans=max(ans,dp[i]);
	cout<<ans;
	return 0;
}


4.代码鉴赏
待完成2026/8/15日前

2026年8月13日

posted @ 2026-08-13 18:11  _11Ly  阅读(1)  评论(0)    收藏  举报