有向图的联通性问题

一、定义

连通:指在无向图中任意两点可以互相到达

强连通:指在有向图中任意两点可以互相到达

强连通分量:有向图的一个子图中任意两点可以互相到达,则称这个子图为该有向图的一个强连通分量(一个点只能同时从属与一个强连通分量)

有向图的联通性问题即求它的强连通分量

二、思路

1.Kosaraju

我们已任意点出发,遍历该有向图,可以得到一个顺序,如图所示:

img

该图后序遍历的顺序示例:(并不唯一)

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难以理解和表述,总之各有所长

最后,如果有错误请帮忙指出,谢谢!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值