Tarjan 算法处理 SCC、割点、割边

Tarjan 算法处理 缩点、割点、割边

强连通分量SCC缩点

有向图进行简化操作,将图中的强连通分量合并为一个节点

合并后的节点具有合并前所有节点的权值贡献且不会重复,对于边的处理当且仅当 SCC 内部有出入边时才映射

例:洛谷P3387 https://www.luogu.com.cn/problem/P3387

const int N=1e4+10;

vector<int> e[N];
vector<int> stk;
bool instk[N];
int dfn[N],low[N],idx;
int scc[N],nw[N],cnt;
vector<int> ne[N];
int dp[N];
int w[N];

void tarjan(int x){
	dfn[x]=low[x]=++idx;
	stk.push_back(x);
	instk[x]=1;
	
	for (auto v:e[x]){
		if (!dfn[v]){
			tarjan(v);
			low[x]=min(low[x],low[v]);
		}else if (instk[v])
			low[x]=min(low[x],dfn[v]);
	}
	
	if (low[x]==dfn[x]){
		int t;
		cnt++;
		do{
			t=stk.back();
			stk.pop_back();
			instk[t]=0;
			scc[t]=cnt;
			nw[cnt]+=w[t];//计算合并后权值
		}while (t!=x);
	}
}

void solve(){
	int n,m;
	cin>>n>>m;
	for (int i=1;i<=n;i++) cin>>w[i];
	for (int i=1;i<=m;i++){
		int u,v;
		cin>>u>>v;
		e[u].push_back(v);
	}
	
	for (int i=1;i<=n;i++)
		if (!dfn[i]) tarjan(i);
		
	for (int i=1;i<=n;i++){
		for (auto v:e[i])
			if (scc[i]!=scc[v])
				ne[scc[i]].push_back(scc[v]);
		dp[i]=nw[i];
	}
	
	int ans=0;
	for (int i=cnt;i>=1;i--){
		for (auto v:ne[i])
			dp[v]=max(dp[v],dp[i]+nw[v]);
		ans=max(ans,dp[i]);
	}
	cout<<ans;
}

首先遍历这张图,Tarjan算法处理出所有的强连通分量:scc[N] ,同时,计算合并节点的总权值 nw[N]

然后对边进行映射:

for (int i=1;i<=n;i++)
	for (auto v:e[i])
		if (scc[i]!=scc[v])//当且仅当这条边不在SCC内才映射
			ne[scc[i]].push_back(scc[v]);

最后计算简化后的图上最大权值路径,因为我们对 SCC 编号是拓扑逆序的,所以最后逆向遍历这张图

采用类似 DP 的方式计算最大权值

int ans=0;
for (int i=cnt;i>=1;i--){
	for (auto v:ne[i])
		dp[v]=max(dp[v],dp[i]+nw[v]);//更新权值
	ans=max(ans,dp[i]);
}

割点

在一张无向图中,删去某个顶点后图中的极大连通分量数增加,那么该点为一个割点

判定法则:如果节点 \(x\) 不是根节点,当且仅当搜索树上存在一个 \(x\) 的子节点 \(y\) 满足 \({\rm{low}}_y\ge {\rm{dfn}}_x\)\(x\) 为割点。如果 \(x\) 是根节点,当且仅当搜索树上至少存在两个子节点 \(y_1,y_2\) 满足 \({\rm{low}}_{y_{1,2}}\ge {\rm{dfn}}_x\)\(x\) 为割点

vector<int> e[N];
int dfn[N],low[N],idx;
int cut[N],root;

void tarjan(int x){
	dfn[x]=low[x]=++idx;
	int son=0;
	for (auto v:e[x]){
		if (!dfn[v]){
			tarjan(v);
			low[x]=min(low[x],low[v]);
			if (low[v]>=dfn[x]){//判断是否为割点的必要条件
				son++;//x的子树+1
				if (x!=root||son>1)//判断根节点的情况
					cut[x]=1;
			}
		}else
			low[x]=min(low[x],dfn[v]);//直接更新
	}
}

void solve(){
	int n,m;
	cin>>n>>m;
	for (int i=1;i<=m;i++){
		int u,v;
		cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	for (int i=1;i<=n;i++){
		if (!dfn[i]){
			root=i;//指定当前连通分量的根节点
			tarjan(i);
		} 
	}
	int sum=0;
	for (int i=1;i<=n;i++) sum+=cut[i];
	cout<<sum<<endl;
	for (int i=1;i<=n;i++)
		if (cut[i]) cout<<i<<' ';
}

特别的,在无向图中不存在横叉边,所以不用考虑 \(v\) 是否在栈内(任何非树边都必然是后向边)因此可以直接用 \({\rm{dfn_v}}\) 更新 \({\rm{low}}_x\)

割边(桥)

在一张无向图中,删去某条边后图中的极大连通分量数增加,那么该点为一个割边或桥

判定法则:当搜索树上存在 \(x\) 的一个子节点 \(y\) 满足 \({\rm{low}}_y>{\rm{dfn}}_x\) 时,这条边为割边

struct edge{int u,v;};
vector<edge> e;
vector<int> h[N];
int dfn[N],low[N],idx;
vector<edge> bri;

void tarjan(int x,int ine){
	dfn[x]=low[x]=++idx;
	for (auto ote:h[x]){
		int v=e[ote].v;
		if (!dfn[v]){
			tarjan(v,ote);
			low[x]=min(low[x],low[v]);
			if (low[v]>dfn[x])
				bri.push_back(e[ote]);
		}else if (ote!=(ine^1))
			low[x]=min(low[x],dfn[v]);
	}
}


void solve(){
	int n,m;
	cin>>n>>m;
	for (int i=1;i<=m;i++){
		int u,v;
		cin>>u>>v;
		e.push_back({u,v});
		h[u].push_back(e.size()-1);
		e.push_back({v,u});
		h[v].push_back(e.size()-1);
	}
	
	for (int i=1;i<=n;i++)
		if (!dfn[i]) tarjan(i,0);
	
	auto cmp=[](edge x,edge y){
		if (x.u!=y.u) return x.u<y.u;
		return x.v<y.v;
	};
	
	sort(bri.begin(),bri.end(),cmp);
	for (auto e:bri){
		if (e.u<e.v)
			cout<<e.u<<' '<<e.v<<endl;
		else
			cout<<e.v<<' '<<e.u<<endl;
	}
}

因为要处理边,所以不采用邻接点的方式存边,而是直接记录边集

struct edge{int u,v;};
vector<edge> e;

而且图是一张无向图,对边进行编号时我们按照连续奇偶序号编号

vector<int> h[N];//节点的出边
e.push_back({u,v});
h[u].push_back(e.size()-1);//按照边加入的顺序编号,从0开始
e.push_back({v,u});
h[v].push_back(e.size()-1);//反向存边

Tarjan算法实现时,通过 h[N] 记录的出边编号建立搜索树

for (auto ote:h[x]){
	int v=e[ote].v;
	if (!dfn[v]){
		tarjan(v,ote);
		low[x]=min(low[x],low[v]);
		if (low[v]>dfn[x])
			bri.push_back(e[ote]);
	}else if (ote!=(ine^1))
		low[x]=min(low[x],dfn[v]);
}

因为对同一条边,正反编号时连续的奇偶,那么可以通过异或运算确定两条边是否为同一条无向边

else if (ote!=(ine^1)) //当这条出边ote边和入边ine不是同一条边时才更新
posted @ 2025-07-14 14:45  才瓯  阅读(21)  评论(0)    收藏  举报