Loading

浅谈有向图的强连通分量

定义

连通分量 \(V\)\(\forall\ u,v\in V\)\(u\) 可以走到 \(v\),且 \(v\) 可以走到 \(u\)

强连通分量 \(V\):指极大的连通分量。也就是说,不存在另一个连通分量 \(V'\),使得 \(V\subsetneqq V'\)

缩点:将所有的强连通分量缩成一个点。

作用:可以通过缩点,将一个有向图转化成一个拓扑图(有向无环图)。

Tarjan

引入时间戳的概念,即在 DFS 搜索中,按照搜索顺序依次给每个点一个编号。

对于一个点 \(u\),定义两个时间戳:

  1. \(\text{dfn}(u)\):遍历到 \(u\) 的时间戳。

  2. \(\text{low}(u)\):从 \(u\) 开始走,即 \(u\)\(u\) 的子树中,所能遍历到最小的时间戳。

性质:若 \(u\) 为其所在子树的最高点,就等价于 \(\text{dfn}(u)=\text{low}(u)\)。这是比较显然的,因为如果 \(u\) 为其所在子树的最高点,那么 \(\text{dfn}(u)\) 一定是子树中最小的,那么 \(\text{low}(u)=\text{dfn}(u)\)

代码模板:

const int N=114514,M=1919810;
int n,m;
int h[N],e[M],ne[M],w[M],idx;
int dfn[N],low[N];
int id[N];//属于哪个强连通分量
int timestamp;//遍历到的时间
stack<int> stk;//存当前还没有遍历完的强连通分量中的所有点
bool instk[N];//是否在栈中
int scccnt;//强连通分量个数
void tarjan(int u){
	dfn[u]=low[u]=++timestamp;
	stk.push(u);
	instk[u]=true;
	for(int i=h[u];~i;i=ne[i]){//遍历u能到达的所有点
		int j=e[i];
		if(!dfn[j]){//这个点没被遍历过
			tarjan(j);
			low[u]=min(low[u],low[j]);//用这个点及其子树的最小值更新u及其子树的最小值
		}
		else if(instk[j]) low[u]=min(low[u],dfn[j]);//dfn[j]一定小于dfn[u]
	}
	if(dfn[u]==low[u]){//u是强连通分量的最高点
		int y;
		scccnt++;
		do{
			y=stk.top();
			stk.pop();
			instk[y]=false;
			//出栈
			id[y]=scccnt;//这个点属于第scccnt个强连通分量
		}while(y!=u);
	}
}

通过代码可以看到,每个点只会被遍历一次,所以 Tarjan 算法的时间复杂度为 \(O(n+m)\)

缩点代码:

for(int i=1;i<=n;i++)
		for(int j=h[i];~j;j=ne[j]){
			int ver=e[i];
			if(id[i]!=id[ver]) add(id[i],id[ver]);//不在一个强连通分量中就加边
		}

例题

原题洛谷原题

如果 \(A\) 认为 \(B\) 是受欢迎的,那么从 \(A\)\(B\) 连一条边。

我们先考虑在拓扑图中,答案是怎样的。如果这个图中有两个及以上的出度为 \(0\) 的点,那么答案就是 \(0\),因为每个点都至少会被一个出度为 \(0\) 的点认为是不受欢迎的。如果有一个出度为 \(0\) 的点,那么答案就是 \(1\),也就是这个点本身。不存在其他情况,因为如果所有点的出度都大于 \(0\),那么必定存在环。

所以我们只需要先缩点,如果是第一种情况,直接输出 \(0\),否则输出那个出度为 \(0\) 的点缩点前有多少点。

参考代码:

#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=10010,M=50010;
int n,m;
int h[N],e[M],ne[M],idx;
int dfn[N],low[N],timestamp;
stack<int> stk;
bool instk[N];
int id[N];
int scccnt;
int siz[N];//表示每个强连通分量中点的数量
int dout[N];//出度
void add(int a,int b){
	e[idx]=b;
	ne[idx]=h[a];
	h[a]=idx++;
}
void tarjan(int u){
	dfn[u]=low[u]=++timestamp;
	stk.push(u);
	instk[u]=true;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(!dfn[j]){
			tarjan(j);
			low[u]=min(low[u],low[j]);
		}
		else if(instk[j]) low[u]=min(low[u],dfn[j]);
	}
	if(dfn[u]==low[u]){
		int y;
		scccnt++;
		do{
			y=stk.top();
			stk.pop();
			instk[y]=false;
			id[y]=scccnt;
			siz[scccnt]++;
		}while(y!=u);
	}
}
int main(){
	cin>>n>>m;
	mems(h,-1);
	while(m--){
		int a,b;
		scanf("%d%d",&a,&b);
		add(a,b);
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i]) tarjan(i);
	for(int i=1;i<=n;i++)
		for(int j=h[i];~j;j=ne[j]){
			int ver=e[j];
			if(id[i]!=id[ver]) dout[id[i]]++;
		}
	int zeros=0,sum=0;//出度为0的点的数量;所有出度为0的强连通分量中点的数量之和
	for(int i=1;i<=scccnt;i++)
		if(dout[i]==0){
			zeros++;
			sum+=siz[i];
			if(zeros>=2){
			    cout<<0;
			    return 0;
			}
		}
	cout<<sum;
	return 0;
}
posted @ 2025-07-25 20:51  liushuangning  阅读(25)  评论(0)    收藏  举报