有向图的联通性问题
一、定义
连通:指在无向图中任意两点可以互相到达
强连通:指在有向图中任意两点可以互相到达
强连通分量:有向图的一个子图中任意两点可以互相到达,则称这个子图为该有向图的一个强连通分量(一个点只能同时从属与一个强连通分量)
有向图的联通性问题即求它的强连通分量
二、思路
1.Kosaraju
我们已任意点出发,遍历该有向图,可以得到一个顺序,如图所示:
该图后序遍历的顺序示例:(并不唯一)
6 7 5 2 3 4 1 8
以逆序遍历它的反图
-
遍历8,顺序为 8,打上标记,不再遍历,则其一个点为强连通分量
-
遍历 1,顺序为 1 2 3 4,打上标记,不再遍历,这4个点为一个强连通分量
-
遍历 5,顺序为 5 3,3已经被标记,所以5为单独的强连通分量
-
遍历7 ,顺序为 7 5,5已经被标记,所以7为单独的强连通分量
-
遍历6 ,顺序为 7 3,均已被标记,所以6也是单独的强连通分量
按照以上思路,可以写出以下模板(已格式化)
int n, cnt, scc[MAXN];//n个点,cnt为scc的个数,scc为所属的强连通分量
bool vis[MAXN];//是否在第一次遍历过
vector <int> v[MAXN], rev[MAXN];//正图和反图
stack <int> s;//存储顺序的栈
void dfs(int now) {
if (vis[now]) return;
vis[now] = 1;
for (auto i : v[now]) dfs(i);
s.push(now);//记录后序遍历的顺序
}
void redfs(int now) {
if (scc[now]) return;
scc[now] = cnt;//若其没有被分配到任何scc中,则其分配至该scc
for (auto i : rev[now]) redfs(i);
}
void Kosaraju() {
while (!s.empty())
s.pop();
memset(scc, 0, sizeof scc);
memset(vis, 0, sizeof vis);
cnt = 0; //清空
for (int i = 1; i <= n; i++) //让每个节点为起点遍历
dfs(i);
while (!s.empty()) { //逆序遍历反图
if (!scc[s.top()]) { //如果未被分配为scc,则其为初始节点遍历
cnt++;
redfs(s.top());
}
}
}
2.Tarjan
我们定义访问到该节点的时间即时间戳dfn[]和遍历过程中可能发生改变的low[],其中low[]的初始值就是时间戳,如果low[]最后的值也为它的时间戳,则说明它是强连通分量的根
这个算法很巧妙,还是先看一下它的过程
对于这个图,我们以1开始遍历,1的时间戳设为1,入栈记录访问顺序
-
遍历到4,4的时间戳设为2,入栈记录访问顺序
-
遍历到3,3的时间戳设为3,入栈记录访问顺序
-
遍历到6,6的时间戳设为4,入栈记录访问顺序,这时它没有边可以访问,且它的low还是dfn,则它为根,在但在它之后没有访问的其它点,所以它单独为scc,出栈
-
回溯到3,3的low[]记录本次访问到的最早访问过的节点,即它本身,3还可以向下遍历到5
-
遍历到5,5的时间戳设为5,入栈记录访问顺序
-
遍历到7,7的时间戳设为6,入栈记录访问顺序
-
6已经被遍历过了,所以回溯到7,7遍历到了6,但已经被分配为了scc,不更新low[],则它为根,但在它之后没有访问的其它点,所以它单独为scc,出栈
-
回溯到5,在它之后访问过7,但7已经被分配为scc,所以它单独为scc,出栈
-
回溯到3,3的low[]记录本次访问到的最早点,即它本身,3还可以向下遍历到2
-
遍历到2,2的时间戳设为7,入栈记录访问顺序
-
1已经遍历过了,所以回溯到2,2的low更新为1的时间戳,它不为根
-
回溯到3,3的low值更新为2的low值即1的时间戳,它不为根
-
回溯到4,4的low值更新为3的low值即1的时间戳,它不为根
-
回溯到1,1的low值不更新,它为根,则2、3、4、1这四个在栈内且在1之前出栈的4个点为scc
-
遍历到8,8的时间戳设为8,入栈记录访问顺序,这时它没有边可以访问,且它的low还是dfn,则它为根,在但在它之后没有访问的其它点,所以它单独为scc,出栈
则按照上述思路代码如下(已格式化)
#include<bits/stdc++.h>
using namespace std;
int ans, scc[10005], cnt, dfn[10005], low[10005], k; //scc是其所属的强连通分量,ans是强连通分量的个数\
low, cnt用于记录时间戳
vector <int> v[10005];//边
stack <int> s;//栈
void Tarjan(int now) {
dfn[now] = low[now] = ++cnt; //初始化时间戳
s.push(now);//入栈记录访问顺序
for (auto i : v[now]) {
if (!dfn[i]) {
Tarjan(i);//递归处理下一个节点
low[now] = min(low[now], low[i]);
} else if (!scc[i]) //如果访问过但没有分配到scc中,则更新low值
low[now] = min(low[now], low[i]);
}
if (low[now] == dfn[now]) { //如果它是根
while (!s.empty() && s.top() != now) { //则在它之后访问到的栈内的节点都为强连通分量里的节点
k = s.top();
s.pop();
scc[k] = dfn[now];
}
ans++;
}
}
三、总结
Tarjan和Kosaraju都是求强连通分量的算法,它们的时间复杂度相同,均为 $ O (n+m)$,Tarjan空间较少,毕竟少建了个图,但Kosaraju更易理解,Tarjan难以理解和表述,总之各有所长
最后,如果有错误请帮忙指出,谢谢!