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;
}