【学习笔记】tarjan 算法大杂烩

前置知识

啊嘿嘿,第三次学习 tarjan 终于是给我学明白了。
先来看一下 dfs 树上的三种边:

  • 树边:从父亲连向儿子的边
  • 返祖边:从儿子连向祖先的边
  • 横叉边:除了前两种边之外的边

tarjan 算法中常用的两个数组:

  • \(dfn_i\):表示 \(i\) 的时间戳,即第几个被遍历到
  • \(low_i\):表示 \(i\) 只经过最多一条返祖边到达的最小时间戳

下面正片开始!

有向图的 tarjan 算法

求强连通分量

这个好像是 tarjan 算法在有向图中的唯一常见应用吧~

强连通分量定义

对于有向图中的一个联通子图 \(G(V,E)\),若子图中的任意两点之间都是可以到达的,则称 \(G(V,E)\) 是一个强连通分量

实现

对着代码理解吧~

dfn[u] = low[u] = ++tim

初始化:显然一个点不经过任何边可以到达的最小时间戳是他自己

if(!dfn[v]){		
	tarjan(v);
	low[u] = min(low[u], low[v]);
}
else if(in[v]) low[u] = min(low[u], dfn[v]);

如果当前点的时间戳没有被算过就说明是树边,否则就是非树边。
对于一条非树边只能用 \(v\)\(dfn\) 去更新当前的 \(low\),否则可以用 \(low\) 来更新当前点的 \(low\)
注意,有向图中的 dfs 树可能会出现横叉边的情况,而横叉边是不可以用来更新 \(low\) 的,所以要特判(用是否在栈内,即是否属于当前子树来判断)。

if(low[u] == dfn[u]){
	++tt;
	int v = st.top();
	while(v != u){
		belong[v] = tt, in[v] = 0;
		scc[tt].pb(v);
		st.pop(), v = st.top();
	}
	belong[v] = tt, in[v] = 0;
	scc[tt].pb(v);
	st.pop();
}

如果遇到 \(dfn\)\(low\) 相等的情况,说明它的子树内还留在栈内的点最靠上就到它了(到不了它的已经被弹掉)。
因此当前点可以到留在栈内的所有点(通过树边),而栈内的所有点都可以到它,所以这些点可以以当前点为中枢,两两联通。
所以它的子树内还留在栈内的点了,与它构成一个极大强连通分量。

code

#include<bits/stdc++.h>
#define pb push_back
#define N 10010
#define Fo(a, b) for(auto a : b) 
#define fo(a, b, c) for(int b = a; b <= c; b++)
using namespace std;
int n, m, dfn[N], low[N], tim, in[N], vis[N], tt, belong[N];
stack<int>st;
vector<int>G[N], scc[N]; 
void tarjan(int u){
	dfn[u] = low[u] = ++tim, in[u] = 1;
	st.push(u);
	Fo(v, G[u]){
		if(!dfn[v]){
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}
		else if(in[v]) low[u] = min(low[u], dfn[v]);
	}
	if(low[u] == dfn[u]){
		++tt;
		int v = st.top();
		while(v != u){
			belong[v] = tt, in[v] = 0;
			scc[tt].pb(v);
			st.pop(), v = st.top();
		}
		belong[v] = tt, in[v] = 0;
		scc[tt].pb(v);
		st.pop();
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n >> m;
	fo(1, i, m){
		int x, y; cin >> x >> y;
		G[x].pb(y);
	}
	fo(1, i, n) if(!dfn[i]) tarjan(i);
	cout << tt << "\n";
	fo(1, i, n){
		if(vis[belong[i]]) continue;
		int u = belong[i];
		vis[u] = 1;
		sort(scc[u].begin(), scc[u].end());
		Fo(v, scc[u]) cout << v << ' ';
		cout << "\n";
	}
	return 0;
} 

应用:缩点

先跑一边 tarjan 求强连通分量,然后将每个强连通分量视为一个点。
将缩点后的不同新点之间连边,建一张新图。
code

#include<bits/stdc++.h>
#define maxn 100010
using namespace std;
int n, m;
struct edge{
	int to, next;
}e[maxn << 1], e1[maxn << 1];
int cnt = 0, a[maxn], head[maxn];
int cnt1 = 0, head1[maxn], ind[maxn], w[maxn];
int tim = 0, low[maxn], tot = 0, dfn[maxn], belong[maxn], d[maxn];
bool vis[maxn];
stack<int>st;
vector<int>scc[maxn];
void add(int x, int y){
	e[++cnt].to = y;
	e[cnt].next = head[x];
	head[x] = cnt;
}
void add1(int x, int y){
	e1[++cnt1].to = y;
	e1[cnt1].next = head1[x];
	head1[x] = cnt1;
}
void tarjan(int u){
	dfn[u] = ++tim;
	low[u] = tim;
	vis[u] = 1;
	st.push(u);
	for(int i = head[u]; i; i = e[i].next){
		int v = e[i].to;
		if(!dfn[v]){
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}
		else if(vis[v]) low[u] = min(low[u], dfn[v]);
	}
	if(dfn[u] == low[u]){
		++tot;
		while(st.top() != u){
			int x = st.top();
			vis[x] = 0;
			belong[x] = tot;
			w[tot] += a[x];
			scc[tot].push_back(x);
			st.pop();
		}
		st.pop();
		vis[u] = 0;
		belong[u] = tot;
		w[tot] += a[u];
		scc[tot].push_back(u);
	}
}
void topo(){
	queue<int>q;
	for(int i = 1; i <= n; i++){
		if(!ind[i]){
			q.push(i);
			d[i] = w[i];
		}
	}
	while(!q.empty()){
		int u = q.front();
		q.pop();
		for(int i = head1[u]; i; i = e1[i].next){
			int v = e1[i].to;
			d[v] = max(d[v], d[u] + w[v]);
			ind[v]--;
			if(!ind[v]) q.push(v);
		}
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n >> m;
	for(int i = 1; i <= n; i++){
		cin >> a[i];
	}
	for(int i = 1; i <= m; i++){
		int x, y;
		cin >> x >> y;
		add(x, y);
	} 
	for(int i = 1; i <= n; i++){
		if(!dfn[i]) tarjan(i);
	}
	for(int u = 1; u <= n; u++){
		for(int i = head[u]; i; i = e[i].next){
			int v = e[i].to;
			int x = belong[u], y = belong[v];
			if(belong[u] != belong[v]){
				add1(x, y);
				ind[y]++;
			} 
		}
	}
	topo();
	int ans = 0;
	for(int i = 1; i <= tot; i++){
		ans = max(ans, d[i]);
	}
	cout << ans;
	return 0;
}

无向图的 tarjan 算法

注意!!,由于无向图的 dfs 树不存在横叉边,所以所有的无向图的 tarjan 算法都不需要判是否在栈内。

求割点

割点定义

通俗的理解为,去掉一个点后,整张无向图就不再联通,那么这个点就可以称为该图的一个割点。

实现

与求强连通分量类似。
注意

if(low[v] >= dfn[u]) flag = 1;

是大于等于号,不是大于号。并且不能直接将推进去,而需要一个 flag 标记,不然会将某些点算重

if(u == rt){
	if(son > 1) ans.pb(rt); 
} 

如果当前点是 dfs 树的根,需要判断它在 dfs 树上是否有两个及以上的儿子。
如果它只有一个儿子,那么去掉它后整张图仍然还是联通的。

code

#include<bits/stdc++.h>
#define fo(a, b, c) for(int b = a; b <= c; b++)
#define Fo(a, b) for(auto a : b)
#define pb push_back
#define N 100010
using namespace std;
int n, m, tim, dfn[N], low[N], rt;
vector<int>G[N], ans;
void tarjan(int u){
	dfn[u] = low[u] = ++tim;
	int son = 0, flag = 0;
	Fo(v, G[u]){
		if(!dfn[v]){
			++son;
			tarjan(v);
			low[u] = min(low[u], low[v]);
			if(low[v] >= dfn[u]) flag = 1;
		}
		else low[u] = min(low[u], dfn[v]);
	}
	if(flag){
		if(u == rt){
			if(son > 1) ans.pb(rt); 
		} 
		else ans.pb(u);
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n >> m;
	fo(1, i, m){
		int x, y; cin >> x >> y;
		G[x].pb(y), G[y].pb(x);
	}
	fo(1, i, n) if(!dfn[i]) rt = i, tarjan(i); 
	cout << ans.size() << "\n";
	sort(ans.begin(), ans.end());
	Fo(u, ans) cout << u << ' ';
	return 0;
}

应用:求点双连通分量

点双定义:

若去掉任意一个点后整张图仍然还是连通的,则这个图是一个点双连通分量。

实现

注意到每个点至少属于一个点双连通分量,且一个割点可能属于多个点双。
所以可以先将割点求出来,每个割点下边必然挂了至少一个点双。
但是注意,这里并不需要判断根节点是否有两个以上的儿子,如果根节点只有一个儿子,则他下边也会挂一个点双。
最后不要忘了判孤立的点(也是点双)。
注意\(u,v\) 之间可能有别的点留着,所以不能弹到 \(u\) 停,而是要弹到 \(v\) 停。

code

#include<bits/stdc++.h>
#define N 100010
#define fo(a, b, c) for(int b = a; b <= c; b++)
#define Fo(a, b) for(auto a : b)
#define pb push_back
using namespace std;
int n, m, dfn[N], low[N], tim, tt, rt;
vector<int>G[N], scc[N];
stack<int>st;
void tarjan(int u){
	dfn[u] = low[u] = ++tim;
	st.push(u);
	int son = 0;
	Fo(v, G[u]){
		if(!dfn[v]){
			++son;
			tarjan(v);
			low[u] = min(low[u], low[v]);
			if(low[v] >= dfn[u]){
				++tt;
				int x = st.top();
				while(x != v){
					scc[tt].pb(x);
					st.pop(), x = st.top();
				}
				st.pop(); 
				scc[tt].pb(v), scc[tt].pb(u);
			}
		}
		else low[u] = min(low[u], dfn[v]);
	} 
	if(!son && u == rt) scc[++tt].pb(u);
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n >> m;
	fo(1, i, m){
		int x, y; cin >> x >> y;
		G[x].pb(y), G[y].pb(x);
	}
	fo(1, i, n) if(!dfn[i]){
		while(!st.empty()) st.pop();
		rt = i, tarjan(i);
	}
	cout << tt << "\n";
	fo(1, i, tt){
		cout << scc[i].size() << ' ';
		Fo(x, scc[i]) cout << x << ' ';
		cout << "\n";
	}
	return 0; 
}

求割边

割边定义

与割点类似,可以理解为,去掉一条边后,整张无向图就不再联通,那么这条边就可以称为该图的一个割边。

实现

与求割边类似。
注意

if(low[v] > dfn[u]) e[id] = 1;

是大于号,不是大于等于号。

else if(id != fa) low[u] = min(low[u], dfn[v]);

需要记录来时边的编号,只能用非反向边的边来更新

code

#include<bits/stdc++.h>
#define N 2000010
#define pb push_back
#define fo(a, b, c) for(int b = a; b <= c; b++)
#define Fo(a, b) for(auto a : b)  
using namespace std;
int n, m, dfn[N], low[N], tim;
int e[N], tt, vis[N]; 
struct edge{
	int id, v;
};
vector<edge>G[N];
vector<int>scc[N];
void tarjan(int u, int fa){
	dfn[u] = low[u] = ++tim;
	Fo(to, G[u]){
		int id = to.id, v = to.v;
		if(!dfn[v]){
			tarjan(v, id);
			low[u] = min(low[u], low[v]);
			if(low[v] > dfn[u]) e[id] = 1;
		}
		else if(id != fa) low[u] = min(low[u], dfn[v]);
	}
}
void dfs(int u){
	scc[tt].pb(u), vis[u] = 1;
	Fo(to, G[u]){
		int v = to.v, id = to.id;
		if(e[id] || vis[v]) continue;
		dfs(v);
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n >> m;
	fo(1, i, m){
		int x, y; cin >> x >> y;
		G[x].pb({i, y}), G[y].pb({i, x});
	}
	fo(1, i, n) if(!dfn[i]) tarjan(i, 0);
	fo(1, i, n) if(!vis[i]){
		++tt, dfs(i);
	}
	cout << tt << "\n";
	fo(1, i, tt){
		cout << scc[i].size() << ' ';
		Fo(x, scc[i]) cout << x << ' ';
		cout << "\n";
	}
	return 0;
}

应用:求边双连通分量

定义

去掉任意一条边后,任然能保持连通的图。

实现

先把所有割边求出来去掉,然后跑 dfs,每个连通块就是一个边双连通分量。

code

放上边了。QAQ

posted @ 2025-10-29 11:03  GuoSN0410  阅读(16)  评论(0)    收藏  举报