Tarjan 算法

有向图的强连通分量 SCC

定义:从其中的任意一个节点出发,都能经过其中的所有点,即其中任意两个节点相通。

遍历方式:DFS 序遍历。

有向边分类:树枝边,前向边,后向边,横叉边。

时间戳追溯值:记 \(dfn[x]\) 为点 \(x\) 的时间戳(被访问的顺序),\(low[x]\) 为点 \(x\) 的追溯值,用它来存储不经过其父亲能到达的最小的时间戳。

强连通分量判定:若从 \(x\) 回溯前,有 \(low[x]=dfn[x]\) 成立,则此时栈中从x到栈顶的所有节点构成一个强连通分量。

代码实现:

const int N=1e4+10;
vector<int> q[N];
stack<int> s;
int dfn[N],low[N],scc[N],siz[N],t,cnt,ans;
void tarjan(int x){
	dfn[x]=low[x]=++t;
	s.push(x);
	for(int i=0;i<q[x].size();i++){
		int y=q[x][i];
		if(!dfn[y]){
			tarjan(y);
			low[x]=min(low[x],low[y]);
		}
		else if(!scc[y]) low[x]=min(low[x],dfn[y]);
	}
	if(dfn[x]==low[x]){
		cnt++;
		while(1){
			int y=s.top();
			s.pop();
			scc[y]=cnt;
			siz[cnt]++;
			if(y==x) break;
		}
	}
}
int main(){
	int n,m,x,y;
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>x>>y;
		q[x].push_back(y);
	}
	for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);

以上代码中,\(cnt\) 计算的是 SCC 的个数(编号),\(scc[i]\) 则记录第 \(i\)个节点属于哪一个 SCC,\(siz[cnt]\) 记录的是每个 SCC 的大小。

因为图不一定连通,所以只要没有被记录时间戳的节点都要 \(Tarjan\) 一遍。

例题:P2863 [USACO06JAN] The Cow Prom S

缩点

我们可以把每个 SCC 缩成一个点。对原图中的每条有向边 \((x,y)\) 进行枚举,如果 \(scc[x]\neq scc[y]\),则在编号为 \(scc[x]\) 与编号为 \(scc[y]\) 的 SCC 连边,构成一张有向无环图,并把这张新图存下来,再进行一系列操作。

代码实现:

for(int i=1;i<=m;i++){
	if(scc[x[i]]!=scc[y[i]]){
		p[scc[x[i]]].push_back(scc[y[i]]);
	}
}

例题:P3387 【模板】缩点P2812 校园网络【[USACO]Network of Schools加强版】P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 GT335409 银河

割点(割顶)

定义:对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。

判定:对于某个顶点 \(x\),如果存在至少一个顶点 \(y\)\(x\) 的子节点),使得 \(low[y] \geq dfn[x]\),即不能回到祖先,那么 \(x\) 点为割点。

对于所有的根节点,则需要两个及以上的子节点,才能作为割点。

代码实现:

const int N=2e4+10,M=1e5+10;
vector<int> q[M];
int root;
int dfn[N],low[N],cut[N],t,cnt;
void tarjan(int x){
	dfn[x]=low[x]=++t;
	int child=0;
	for(int i=0;i<q[x].size();i++){
		int y=q[x][i];
		if(!dfn[y]){
			tarjan(y);
			low[x]=min(low[x],low[y]);
			if(low[y]>=dfn[x]){
				child++;
				if(x!=root||child>1) cut[x]=1;
			}
		}
		else low[x]=min(low[x],dfn[y]);
	}
}
int main(){
	int n,m,a,b;
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>a>>b;
		q[a].push_back(b);
		q[b].push_back(a);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]){
			root=i;
			tarjan(i);
		}
	}

以上的代码将所有的割点标记为了 \(1\)。注意每次做 \(Tarjan\) 的时候将 \(root\) 赋值为 \(i\)

例题:P3388 【模板】割点(割顶)

割边(桥)

定义:对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。

判定:与割点差不多,改成 \(low[y]>dfn[x]\) 就可以了,不需要考虑根节点的问题,但是需要判断不能重复经过已走路径(往回走)。

代码实现:

const int N=1e4+10;
vector<int> q[N];
int dfn[N],low[N],t,cnt,idx;
struct node{
	int x,y;
}ans[N];
bool cmp(node m,node n){
	if(m.x!=n.x) return m.x<n.x;
	return m.y<n.y;
}
void tarjan(int x,int fa){
	dfn[x]=low[x]=++t;
	int child=0;
	for(int i=0;i<q[x].size();i++){
		int y=q[x][i];
		if(!dfn[y]){
			tarjan(y,x);
			low[x]=min(low[x],low[y]);
			if(low[y]>dfn[x]) ans[++idx].x=x,ans[idx].y=y;
		}
		else if(dfn[y]<dfn[u]&&y!=fa) low[x]=min(low[x],dfn[y]);
	}
}
int main(){
	int n,m,a,b;
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>a>>b;
		q[a].push_back(b);
		q[b].push_back(a);
	}
	for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,-1);
	sort(ans+1,ans+idx+1,cmp); 
	for(int i=1;i<=idx;i++) cout<<ans[i].x<<' '<<ans[i].y<<endl;
	return 0;
}

程序输出即为所有割边。

例题:P1656 炸铁路

边双连通

定义:在一张连通的无向图中,对于两个点 \(x\)\(y\),如果删去任意一条边都不能使它们不连通,我们就说 \(x\)\(y\) 边双连通。

特征:具有传递性,若 \(x,y\) 边双连通,\(y,z\) 边双连通,则 \(x,z\) 边双连通。

边双连通分量 e-DCC

定义:对于一个无向图中的 极大 边双连通的子图,则称这个子图为一个 边双连通分量

过程:与求强连通分量类似。

代码实现:

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
struct node{
	int y,w;
};
vector<node> q[N];
vector<int> ans[N];
stack<int> s;
int dfn[N],low[N],scc[N],siz[N],t,cnt;
void tarjan(int x,int last){
	dfn[x]=low[x]=++t;
	s.push(x);
	for(int i=0;i<q[x].size();i++){
		int y=q[x][i].y,w=q[x][i].w;
		if(w==(last^1)) continue;//成对变换(2^1=3,4^1=5...),避免走反边
		if(!dfn[y]){
			tarjan(y,w);
			low[x]=min(low[x],low[y]);
		}
		else low[x]=min(low[x],dfn[y]);
	}
	if(dfn[x]==low[x]){
		ans[++cnt].push_back(x);
		while(1){
			int y=s.top();
			s.pop();
			ans[cnt].push_back(y);
			scc[y]=cnt;
			siz[cnt]++;
			if(y==x) break;
		}
	}
}
int main(){
	int n,m,a,b;
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>a>>b;
		q[a].push_back({b,i<<1});
		q[b].push_back({a,i<<1|1});//给两条边建立成对下标,方便判重(见第16行)
	}
	for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0);
	cout<<cnt<<endl;
	for(int i=1;i<=cnt;i++){
		cout<<siz[i];
		for(int j=0;j<siz[i];j++){
			cout<<' '<<ans[i][j];
		}
		puts("");
	}
	return 0;
}

例题:P8436 【模板】边双连通分量P2860 [USACO06JAN] Redundant Paths G

点双连通

定义:在一张连通的无向图中,对于两个点 \(x\)\(y\),如果删去任意一个点(不包括 \(x,y\) 本身)都不能使它们不连通,我们就说 \(x\)\(y\) 点双连通。

特征:不具有传递性,存在 \(x,y\) 点双连通,\(y,z\) 点双连通,而 \(x,z\) 不点双连通。

点双连通分量 v-DCC

定义:对于一个无向图中的 极大 点双连通的子图,我们称这个子图为一个 点双连通分量

性质

\(1\). 两个点双最多只有一个公共点,且一定是割点。

\(2\). 对于一个点双,它在 DFS 搜索树中 dfn 值最小的点一定是割点或者树根。

结论

\(1\). 当这个点为割点时,它一定是点双连通分量的根,因为一旦包含它的父节点,他仍然是割点。

\(2\). 当这个点为树根时:有两个及以上子树,它是一个割点;只有一个子树,它是一个点双连通分量的根;它没有子树,视作一个点双。

代码实现:

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10,M=2e6+10;
stack<int> s;
vector<int> q[N],dcc[M];
int root,cut[N];
int dfn[N],low[N],scc[N],siz[N],t,cnt;
void record(int x,int y){
	cnt++;
	while(1){
		int t=s.top();
		s.pop();
		dcc[cnt].push_back(t);
		if(t==y) break;
	}
	dcc[cnt].push_back(x);
}
void tarjan(int x){
	dfn[x]=low[x]=++t;
	s.push(x);
	if(x==root&&!q[x].size()){//孤点
		dcc[++cnt].push_back(x);
		return;
	}
	int child=0;
	for(int i=0;i<q[x].size();i++){
		int y=q[x][i];
		if(!dfn[y]){
			tarjan(y);
			low[x]=min(low[x],low[y]);
			if(low[y]>=dfn[x]){
				child++;
				if(child>1||x!=root) cut[x]=1;
				record(x,y);
			}
		}
		else low[x]=min(low[x],dfn[y]);
	}
}
int main(){
	int n,m,a,b;
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>a>>b;
		if(a==b) continue;//重边
		q[a].push_back(b);
		q[b].push_back(a);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]){
			root=i;
			tarjan(i);
		}
	}
	cout<<cnt<<endl;
	for(int i=1;i<=cnt;i++){
		cout<<dcc[i].size();
		for(int j=0;j<dcc[i].size();j++) cout<<' '<<dcc[i][j];
		cout<<endl;
	}
	return 0;
}

对于代码中的第 \(34\) 行的 \(record\) 函数,在此特别进行说明。

已经判断出以 \(x\) 为根的子树 \(y\) 为一个 v-DCC ,此时要把这个子树存下来。

为什么只到节点 \(y\) 就 break 掉呢?因为 \(x\) 可能有多个子树,直接在栈里找到根 \(x\) 可能会把 \(x\) 的其他子节点存入,不能保证其正确性。所以此处存到 \(y\) 后就 break 掉,再存入根 \(x\)

例题:P8435 【模板】点双连通分量

posted @ 2025-10-02 21:34  筝小鱼  阅读(8)  评论(0)    收藏  举报