Tarjan求强连通分量

前言:

自本文章开始,作者会着重讲述关于\(Tarjan\)的算法问题与题目类型


强连通分量

强连通图指的是在一个有向图中,任意两个点都能相互到达,则称这个图为强连通图

强连通分量指的是一个图中的强连通子图,例如下图:

屏幕截图 2025-04-26 175053

图中的[3,4,5,6]这4个点构成了一个强连通分量,因为这四点中任意两点都可以相互到达(其实就是一个环)


\(DFS\)生成树

屏幕截图 2025-04-26 175733

有向图的 DFS 生成树主要有 4 种边(不一定全部出现):

  1. 树边(tree edge):示意图中以黑色边表示,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
  2. 反祖边(back edge):示意图中以红色边表示,也被叫做回边,即指向祖先结点的边。
  3. 横叉边(cross edge):示意图中以蓝色边表示,它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先。
  4. 前向边(forward edge):示意图中以绿色边表示,它是在搜索的时候遇到子树中的结点的时候形成的。

这里作者直接搬运的oi-wiki上的内容


$ Tarjan $算法

引入

\(Robert E. Tarjan\)(罗伯特·塔扬,1948~),生于美国加州波莫纳,计算机科学家。

算法实现

我们要对图进行一个\(dfs\)搜索,在搜索中我们要维护两个数组\(dfn\)\(low\)

  • \(dfn\)表示这个点在搜索中被搜索的次序,即对这个点进行一个标号,满足一个性质即一个结点的子树内结点的\(dfn\)都大于该结点的 \(dfn\)
  • \(low\)表示这个点所能到达的\(dfn\)值最小的一个点

这里对\(low\)的作用进行一下解释,\(low\)的本质就是一个不断向上跳的过程,因为一个结点的子树内结点的\(dfn\)都大于该结点的 \(dfn\),所以该节点正常来说\(low\)是不会大于\(dfn\)值的,但是在\(dfs\)树中会存在非树边,所以\(low\)值可能会小于该节点的\(dfn\)值,此时我们会发现因为非树边的存在,该图就可能会出现强连通分量,所以我们也可以把整个过程看作求\(dfs\)树中的返祖边(不理解的可以再看看上面的图)

以上就是$ Tarjan $的核心逻辑,下面开始具体实现

按照\(dfs\)搜索的次序对图中所有的结点进行搜索,维护每个结点的 \(dfn\)\(low\) 变量,且让搜索到的结点入栈。每当找到一个强连通元素,就按照该元素包含结点数目让栈中元素出栈。

此时我们还要分析三种情况:

  • v未被访问过,即\(dfn\)为空,则访问该结点,并更新\(low_u\),取\(low_u\)\(low_v\)的最小值

  • v被访问过,即已经在栈中,继续更新\(low_u\),取\(low_u\)\(dfn_v\)的最小值

    • 这里解释以下为什么是取\(low_u\)\(dfn_v\)的最小值,而不是取\(low_u\)\(low_v\)的最小值,因为该点已经被访问所以这条边只会有两种情况,一种是反祖边,一种是横叉边,无论哪种,\(dfn_v\)值必定小于\(dfn_u\),如果取该点的\(low\)值,很可能导致\(low_u\)的值往上跳的太多,不能判断是否构成了环
  • v被访问过,已不在栈中,说明该点是强连通中的一个点,不用考虑

下面是示例代码

void Tarjan(ll u) {
	deep++;//dfs序
	low[u] = dfn[u] = deep;//初始化low与dfn
	st.push(u);//将该点压入栈
	vis[u]=true;//标记该点,说明已被访问,在栈中
	for(auto v:g[u]){
		if(!dfn[v]){//对应情况1
			Tarjan(v);//先对v点进行搜索,让v点取得最小的low值,在进行更新
			low[u]=min(low[v],low[u]);
		}	
		else{
			if(vis[v]){//对应情况2,更新low值
				low[u]=min(low[u],dfn[v]);
			}
		}
	}
	if(low[u]==dfn[u]){//说明这里构成了一个环,出现了强连通分量
		SCCcnt++;//强连通分量个数加一
		while(1){//开始标记强连通分量中的点
			ll t=st.top();//取出栈顶元素
			st.pop();
			vis[t]=false;//标记该点已经不在栈中
			SCCid[t]=SCCcnt;//标记该点所属强连通分量的编号
			if(u==t)break;//如果标记到了自己,说明该强连通分量已经标记完毕,结束循环
		}
	}
}

时间复杂度\(O(n+m)\)

例题:[P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G]([P2341 USACO03FALL / HAOI2006] 受欢迎的牛 G - 洛谷)

示例代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;

ll dfn[N], low[N], vis[N], SCCid[N], out[N];
vector<ll> g[N];
ll deep,SCCcnt;
stack<ll> st;

void Tarjan(ll u) {
	deep++;
	low[u] = dfn[u] = deep;
	st.push(u);
	vis[u]=true;
	for(auto v:g[u]){
		if(!dfn[v]){
			Tarjan(v);
			low[u]=min(low[v],low[u]);
		}	
		else{
			if(vis[v]){
				low[u]=min(low[u],dfn[v]);
			}
		}
	}
	if(low[u]==dfn[u]){
		SCCcnt++;
		while(1){
			ll t=st.top();
			st.pop();
			vis[t]=false;
			SCCid[t]=SCCcnt;
			if(u==t)break;
		}
	}
}

int main(){
	ll n,m;
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		ll u,v;
		cin>>u>>v;
		g[u].push_back(v);
	}
	for (int i = 1; i <= n; i++) {
		if (!dfn[i]) Tarjan(i);
	}
	for(int i=1;i<=n;i++){
		for(int v:g[i]){
			if(SCCid[i]!=SCCid[v]){
				out[SCCid[i]]++;
			}
		}
	}
	int zeroOutCnt = 0, ansScc = 0;
	for (int i = 1; i <= SCCcnt; i++) {
		if (out[i] == 0) {
			zeroOutCnt++;
			ansScc = i;
		}
	}
	if (zeroOutCnt == 1) {
		int ans = 0;
		for (int i = 1; i <= n; i++) {
			if (SCCid[i] == ansScc) ans++;
		}
		cout << ans << endl;
	} else {
		cout << 0 << endl;
	}
}
posted @ 2025-04-26 18:44  Accepted_wyr  阅读(83)  评论(0)    收藏  举报