【Tarjan】强连通分量与SCC缩点

1 强连通分量的概念

1)强连通:若一张有向图的节点两两互相可达,则称这张图是强连通的。
2) 强连通分量:极大图的强连通子图。

2 Tarjan算法求图中的强连通分量

2.1 变量说明

① 时间戳dfn数组:节点x第一次被访问的顺序
② 追溯值low数组:从节点x出发,所能访问到的最早时间戳
③ num数组:num数组记录每个强连通分量的节点数。
④ color数组:我们假设把图中的每个强连通分量染成不同的颜色,color数组记录每个节点被染的颜色变化。
⑤ used数组:记录每个节点是否加入某个强连通分量。
⑥ vis数组:记录在扩展某个强连通分量过程中节点是否被访问过。
⑦ G数组:邻接表
⑧ 栈s:存储属于某个强连通分量的节点
⑨ 变量cnt:存储当前遍历节点的时间戳,初始化为0

2.2 算法流程

① 输入n,m,表示有n个节点,m条有向边。
② 输入m条有向边的信息,并将信息存储在邻接表中。
③ 遍历邻接表,如果节点未加入,则进入④(代码中用tarjan函数实现),求该节点的强连通分量。
④ tarjan算法
对于节点x,
step1:有新节点遍历,cnt=cnt+1。令dfn[x]=low[x]=cnt。
step2:更新该节点在used数组和vis数组的值,vis[x]=used[x]=1。
step3:遍历x邻接的节点,记为节点q。如果节点q没访问过,则让q入栈,递归调用tarjan函数,调用完后,更新low[x],low[x]=min(low[x],low[q])。如果q在之前已经访问过,说明q在更早的时候被遍历过。则更新low[x],low[x]=min(low[x],dfn[q])。
step4:遍历完所有邻接的节点后,如果low[x]==dfn[x],这时一个强连通分量已经确定,colornum++。我们将这一轮入栈的节点染色(代码中调用paint函数):记录后color数组和num数组。并且染色后令这一轮入栈的节点的vis数组元素值为0。

放几张董晓老师的图,便于理解:
在这里插入图片描述
在这里插入图片描述

2.3 算法应用

2.3.1 洛谷P2863(模版)

题目链接
这道题使用Tarjan算法求强连通分量之后,可以遍历num数组,统计值大于1的元素数量。

AC代码

#include<bits/stdc++.h>
#define maxn 50005
using namespace std;
int n,m,color[maxn],colornum,dfn[maxn],low[maxn],cnt;
bool vis[maxn],used[maxn];
vector<int> G[maxn];
stack<int> s;

void paint(int x){
	s.pop();
	color[colornum]++;
	vis[x]=0;
} 
void tarjan(int x){
	used[x]=1;
	vis[x]=1;
	cnt++;
	dfn[x]=cnt;
	low[x]=cnt;
	for(int i=0;i<G[x].size();i++){
		int q=G[x][i];
		if(dfn[q]==0){
			s.push(q);
			tarjan(q);
			low[x]=min(low[x],low[q]);
		}
		else if(vis[q])
			low[x]=min(low[x],dfn[q]);
	}
	if(low[x]==dfn[x]){
		colornum++;
		while(s.top()!=x){
			int t=s.top();
			paint(t);
		}
		paint(x);
	}
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;
		cin>>u>>v;
		G[u].push_back(v);
	}
	for(int i=1;i<=n;i++){
		if(used[i]==0){
			s.push(i);
			tarjan(i);
		}
	}
	int ans=0;
	for(int i=1;i<=colornum;i++){
		//cout<<color[i]<<endl;
		if(color[i]>1) ++ans;
	}
	cout<<ans;
    return 0;
}

2.3.2 洛谷P2341

题目链接
这道题在求强连通分量的基础上,我们还用到了缩点。一个强连通分量可以理解为一个缩点。我们统计出度为0的缩点数量,若出度为0的缩点数量只有1个,那么所求答案即为该缩点所包含的节点数量。如果缩点数量大于1,则答案为零。这里也借用董晓老师的图来说明一下:
在这里插入图片描述
图中,上面的②→①,答案为缩点①1中包含的节点数量。下图,因为有两个分叉,①和②没有相连的边,找不到“明星奶牛”。

AC代码

#include<bits/stdc++.h>
#define maxn 50005
using namespace std;
int dfn[maxn],low[maxn],m,n,color[maxn],num[maxn],colornum,cnt,din[maxn],dout[maxn];
bool used[maxn],vis[maxn];
stack<int> s;
vector<int> G[maxn],H[maxn];
void paint(int x){
	s.pop();
	color[x]=colornum;
	num[colornum]++;
	vis[x]=0;
}
void tarjan(int x){
	cnt++;
	dfn[x]=low[x]=cnt;
	used[x]=vis[x]=1;
	for(int i=0;i<G[x].size();i++){
		int q=G[x][i];
		if(dfn[q]==0){
			vis[q]=1;
			s.push(q);
			tarjan(q);
			low[x]=min(low[x],low[q]);
		}		
		else if(vis[q]) low[x]=min(low[x],dfn[q]);
	}
	if(low[x]==dfn[x]){
		int t=s.top();
		colornum++;
		while(t!=x){
			paint(t);
			t=s.top();
		}
		paint(x);
	}
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;
		cin>>u>>v;
		G[u].push_back(v);
		H[v].push_back(u);
	}	
	for(int i=1;i<=n;i++){
		if(used[i]==0){
			used[i]=1;
			s.push(i);
			tarjan(i);
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=0;j<G[i].size();j++){
			if(color[i]!=color[G[i][j]])
				dout[color[i]]++;
		}
	}
	int ans=0,z=0;
	for(int i=1;i<=colornum;i++){
		if(dout[i]==0) {
			ans=num[i];	z++;
		}
		if(z>1) ans=0;
	}
	cout<<ans<<endl;
    return 0;
}

2.3.3 洛谷P2812

题目链接
这道题使用tarjan算法后,第一问的答案找的是入度为0的节点数量,第二问的答案,首先要做特判,如果只有一个缩点,则不需要加线路所有电脑即可访问,否则答案为入度为0的节点数量和出度为零的节点数量的较大值。

AC代码

#include<bits/stdc++.h>
#define maxn 500005 
using namespace std;
int din[maxn],dout[maxn],dfn[maxn],low[maxn],cnt,color[maxn],num[maxn],colornum,m,n;
bool vis[maxn],used[maxn];
vector<int> G[maxn];
stack<int> s;
void paint(int x){
	s.pop();
	vis[x]=0;
	num[colornum]++;
	color[x]=colornum;
} 
void tarjan(int x){
	used[x]=vis[x]=1;
	cnt++;
	dfn[x]=low[x]=cnt;
	for(int i=0;i<G[x].size();i++){
		int q=G[x][i];
		if(dfn[q]==0){
			vis[q]=1;
			s.push(q);
			tarjan(q);
			low[x]=min(low[q],low[x]);
		}
		else if(vis[q]==1) low[x]=min(low[x],dfn[q]);
	}
	if(dfn[x]==low[x]){
		colornum++;
		while(x!=s.top()){
			int t=s.top();
			paint(t);
		}
		paint(x);
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		int v;
		cin>>v;
		while(v!=0){
			G[i].push_back(v);	
			cin>>v;
		}
	}
	for(int i=1;i<=n;i++){
		if(used[i]==0){
			s.push(i);
			tarjan(i);
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=0;j<G[i].size();j++){
			if(color[i]!=color[G[i][j]]){
				din[color[G[i][j]]]++;
				dout[color[i]]++;
			}
		}
	}
	int a=0,b=0; //a统计入度为0的节点数量,b统计出度为0的节点数量。 
	for(int i=1;i<=colornum;i++){
		if(din[i]==0) a++;
		if(dout[i]==0) b++;
	}
	cout<<a<<endl;
	if(colornum==1) cout<<0<<endl;
	else cout<<max(a,b)<<endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值