理解有向图的强连通分量
每两个顶点都至少有一条路径(A->B,B->A),然后称这两个顶点具有强联通性,这两个顶点组成的图为强连通图。
从这个途中看到,阴影部分是强连通图,拿6看,6与7不是强连通图的原因是7到6可以,但是6到7找不到一条路径。
Tarjan算法
对tarjan对计算强连通分块计算理解:
对核心部分(伪代码)的下图理解
for i in (u,v){ //u指向v,下面都说成相邻了
if v 未被访问:
dfs(v)
low[u] = min(low[u],low[v]); //跳出来之后刷新最值
else if v 被访问过:
low[u] = min(low[u],dfn[v]);
}
下面就是对上面这几行代码的图示
首先给传入了节点1,遍历1附近的点,先遍历到了2,3未被访问,进栈;
然后遍历3附近的点,先遍历到了5,5未被访问,进栈;
然后遍历5附近的点,先遍历到了6,6未被访问,进栈;
(此时是初始化了这几个节点的low,但是由于回溯的时候还会更新,所有此时并未写出)
然后遍历6附近没有(就没有进6附近遍历的循环),而是直接进入下面的判断该节点是否为强连通分量,这个节点dfn == low,所以6为强连通分量
此时num++,变成1,size[1]记录了6这个连通分量的大小
然后退到让一层,此时刷新节点5的low值,经过比较,low[5] = 3,low[to[i]] = 4,所以还是本身的3。然后这一层的for遍历的邻近的节点只有6,遍历完了,就跳出了,也进入了判断该节点是否为强连通分量,这个节点dfn == low,所以5为强连通分量。
此时num++,变成2,size[2]记录了5的强连通分量
然后5遍历完了之后再往回回溯,到3(这里还有一个刷新当前的low的值的过程,low[3] = 2,low[to[i]] = 3,所以不变),3那层的for循环遍历的邻近节点还有一个4,发现4为未被访问,
然后访问4,然后遍历4的所有邻近的节点,发现1和6都是已经被访问过的了
被访问就代表已在栈中,然后刷新low值比较的是当前这层的low[4] = 5 和dfnt[to[i]](也就是节点1和节点6的dfn进行比较取小的),最后刷新出节点4的low[] = 1;
然后回溯,3的low被4的low刷新为1,然后返回节点1。
节点1那一层遍历的邻近节点还有2未被访问。
访问2,然后遍历发现4被访问,然后比较2节点的low[2] = 6与dfnt[to[i]](也就是节点4的dfn = 5比较),最后刷新low[2] = 5。
然后回溯到1,1的low也被比较了一次,但并未刷新,
最后节点1跳出那一层的循环,然后判断该节点是否为强连通分量。
num++,变为3,size[3]储存是1,2,3,4这个强连通分量。
准备:
- 一个数组来模拟栈:用来暂时储存节点,然后通过对条件的判断是否进出栈,最后留在栈中的是一个强联通分量的图的所有节点,所以有最后退栈的操作,即看这个连通图的组成部分。
- 储存图中关系的方法(spfa 网络流费用流有时也会用到这种储存方法):
- 有向图(有方向性):
head[maxn] ,nt[maxn], to[maxn
(后面这两个数组要开大一点,防止爆掉) - 在储存数据的时候将指向关系通过数组存起来
- 有向图(有方向性):
void addedge(int a,int b){
nt[++cnt] = head[a];
head[a] = nt[cnt];
to[cnt] = b;
}
-
遍历一遍这个点连接的其他所有点
-
传进来了一个节点,然后遍历这个节点相邻的其他所有点
//u就是传进来的那个点
for(i = head[u];i;i =nt[i]){
//to[i]是拿到的那个相邻的点
}
- 用num来记录强连通分块的个数,用size[]来储存所有的强连通分块的元素的个数
代码:
//
// main.cpp
// 强连通分量_模版_cow
//
// Created by 陈冉飞 on 2019/8/14.
// Copyright © 2019 陈冉飞. All rights reserved.
//
#include <iostream>
using namespace std;
int n,m,a,b,ch,ans; //注意声明i的时候别声明全局
#define maxn 10005
int head[maxn],nt[maxn*5],to[maxn*5],cnt; //记录点的坐标
int in[maxn],stac[maxn],top,num,size[maxn]; //开一个栈来储存当前的点,top是顶上的点
int dfn[maxn],low[maxn],tim;
int chu[maxn];
void addedge(int a,int b){
nt[++cnt] = head[a];
head[a] = cnt;
to[cnt] = b;
}
void dfs(int k){
stac[++top] = k;
dfn[k] = low[k] = ++tim;
//往后一直延伸 访问这个节点所有相连的节点
for (int i = head[k]; i; i = nt[i]) {
// cout<<i<<endl;
if(!dfn[to[i]]){ //如果没有访问
dfs(to[i]); //访问,然后回溯回去
low[k] = min(low[k],low[to[i]]);
}else if(!in[to[i]]) low[k] = min(low[k],dfn[to[i]]); //被访问了,回溯
}
if (low[k] == dfn[k]) { //子树为强连通分量,就要一直退栈
++num;
while(stac[top] != k){
in[stac[top--]] = num;
++size[num];
}
in[stac[top--]] = num;
size[num]++;
}
}
int main(int argc, const char * argv[]) {
cnt = 0;
scanf("%d%d",&n,&m);
for (int i = 1; i <= m; i++) {
scanf("%d%d",&a,&b);
addedge(a,b);
}
//检验输出
// for (int i = 1; i <= n; i++) {
// cout<<i<<" "<<nt[i]<<" "<<head[i]<<" "<<to[i]<<endl;
// }
for (int i = 1; i <= n; i++)
if (!in[i]) dfs(i);
// cout<<"强连通块的个数"<<num<<endl;
// for (int i = 1; i <= num; i++) cout<<i<<" "<<size[i]<<endl;
for (int i = 1; i <= n; i++)
for (int j = head[i]; j; j = nt[j])
if (in[i] != in[to[j]]) chu[in[i]]++;
for (int i= 1; i <= num; i++) {
if (chu[i]) continue;
if (ch++) {cout<<"0"<<endl;return 0;}
ans = size[i];
}
cout<<ans<<endl;
return 0;
}
注意这种dfs递归的i这种循环里经常用到的索引就不要声明为全局,哪一层改i就会引起其他层的i变化,所以不如多费点空间,让每一层都有个对应的i,防止死递归。(一开始声明全局i,看了将近一个小时愣是没看出来)