强连通分量

强连通分量

强连通定义

有向图 G G G 的强连通是指 G G G 中任意两个节点都可以直接或间接到达。

下方两幅图都是强连通。一个特殊一点,任意两点都可以直接到达;一个则是最常见的强连通图。

  • 特殊强连通图,任意两点都可以直接到达
  • 常见的强连通图,即一个环

强连通分量

强连通分量,简称 S C C SCC SCC,是在一个有向图中最大的强连通子图。

  • 图中加粗的点组成的子图即为此图的强连通分量

dfs 生成树的边

dfs 遍历过程就不多说了,这是图论基本能力。

dfs 深搜后,会出现四种不同情况的边,如下:

  • 树边:由 dfs 自然搜索到的边,组成一棵树(不一定是最小/大生成树)。
  • 返祖边(回边):由一个节点指向前面已经遍历过的祖先节点的边。
  • 横叉边:指向了一个访问过但不是当前节点的祖先的边。
  • 前向边:指向了目前未遍历到的节点,但以后会遍历到的节点的边。

例如,下图即为一张图 G G G。

不难看出,dfs 生成树长这样:

对比一下,如下:

黑边为树边,是正常深搜而来的。

红边即为返祖边,因为它指向了当前节点 7 7 7 的祖先。

蓝边即为横叉边,因为它指向了当前生成树的另一个节点,但不是当前节点 9 9 9 的祖先。

绿边即为前向边,指向了还未加入生成树的节点。

Tarjan 算法

Tarjan 算法是用来求解 S C C SCC SCC 的著名算法,可以在线性时间复杂度完成统计 S C C SCC SCC 的任务。

思路

若节点 u u u 是 S C C SCC SCC 在搜索树中访问到的第一个节点,那么 S C C SCC SCC 就肯定是一个以 u u u 为根节点的子树,我们称 u u u 为这个 S C C SCC SCC 的根。

Tarjan 算法基本思路为把每个 S C C SCC SCC 都看作搜索树的一个子树,将其节点一个个保存。

对于两个节点 u u u 和 v v v,若 u u u 是 v v v 的祖先,且 v v v 有一条返祖边能指向 u u u,则 u u u 和 v v v 形成了环,属于一个 S C C SCC SCC。从 u u u 到 v v v 一路上遇到的所有点也属于这一 S C C SCC SCC 中的点,边也为 S C C SCC SCC 中边。

步骤

每次遍历到一个节点 u u u,需要统计一下信息:

  • dfn[u],即 u u u 的时间戳(第几个被访问到的)。
  • low[u], u u u 属于的那个 S C C SCC SCC 中 dfn 最小的时间戳。

初始化时,dfn[u]=low[u]=++tottot 为时间戳。

既然 dfs 是一种递归的算法,不妨用栈来存节点信息。

每次搜到一个节点都将其入栈,有出度则沿着出度遍历。

上文说到,dfs 搜索会搜到 4 4 4 种边,那么我们该如何解决 4 4 4 种边呢?

  • 树边:正常搜
  • 返祖边:更新当前的 low
  • 横叉边:无视,没用
  • 前向边:无视,没用

每次搜完子树都需要更新 u u u 的 low 值,若 low[u]==dfn[u],则 u u u 为这个 S C C SCC SCC 的根节点,因为没有比他时间戳更小的了(回溯完之后)。

例题:福州一中OJ P2110 求有向图的强连通分量

AC Code:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5,MAXM=8e5+5;
struct EDGE{
	int to,pre;
}edge[MAXM<<1];
int head[MAXN],cnt_edge,tot,t;
int n,m,op;
//链式前向星存图 
void add(int from,int to)
{
	edge[++cnt_edge].to=to;
	edge[cnt_edge].pre=head[from];
	head[from]=cnt_edge;
	return;
}
int dfn[MAXN],low[MAXN];
stack<int> st;
bool vis[MAXN];
int cnt_ans,cnt_t,maxn;
void dfs(int u)//目标是统计maxn 
{
	dfn[u]=low[u]=++tot;//时间戳和子树最小时间戳 
	st.push(u);
	vis[u]=true;
	for(int i=head[u];i;i=edge[i].pre)
	{
		if(!dfn[edge[i].to])
		{
			dfs(edge[i].to);
			low[u]=min(low[u],low[edge[i].to]);//更新 
		}
		else
			if(vis[edge[i].to])//返祖边,注意是vis,不是dfn(有可能是横叉边) 
				low[u]=min(low[u],dfn[edge[i].to]);
	}
	if(low[u]==dfn[u])//是SCC的根节点 
	{
		cnt_t=0;//统计SCC节点个数 
		do{//记得是先做在判断 
			vis[t=st.top()]=false;//比u后入栈的都是SCC的子节点 
			st.pop();
			cnt_t++;
		}while(u!=t);
		maxn=max(maxn,cnt_t);
	}
	return;
}
void dfs2(int u)//与dfs大同小异,目标是计算有多少个强连通子图 
{
	dfn[u]=low[u]=++tot;
	st.push(u);
	vis[u]=true;
	for(int i=head[u];i;i=edge[i].pre)
	{
		if(!dfn[edge[i].to])
		{
			dfs2(edge[i].to);
			low[u]=min(low[u],low[edge[i].to]);
		}
		else
			if(vis[edge[i].to])
				low[u]=min(low[u],dfn[edge[i].to]);
	}
	if(low[u]==dfn[u])
	{
		cnt_t=0;
		do{
			vis[t=st.top()]=false;
			st.pop();
			cnt_t++;
		}while(u!=t);
		if(cnt_t==maxn)
			cnt_ans++;
	}
	return;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&op);
		while(op--)
		{
			scanf("%d",&t);
			add(i,t);
		}
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i])
			dfs(i);
	//初始化 
	while(!st.empty())
		st.pop();
	for(int i=1;i<=n;i++)
	{
		dfn[i]=0;
		low[i]=0;
		vis[i]=false;
	}
	//初始化 
	for(int i=1;i<=n;i++)
		if(!dfn[i])
			dfs2(i);
	printf("%d %d\n",maxn,cnt_ans);
	return 0;
}
相关推荐
会员源码网32 分钟前
使用`mysql_*`废弃函数(PHP7+完全移除,导致代码无法运行)
后端·算法
木心月转码ing1 小时前
Hot100-Day10-T438T438找到字符串中所有字母异位词
算法
HelloReader2 小时前
Wi-Fi CSI 感知技术用无线信号“看见“室内的人
算法
颜酱5 小时前
二叉树分解问题思路解题模式
javascript·后端·算法
qianpeng8976 小时前
水声匹配场定位原理及实验
算法
董董灿是个攻城狮18 小时前
AI视觉连载8:传统 CV 之边缘检测
算法
blasit1 天前
笔记:Qt C++建立子线程做一个socket TCP常连接通信
c++·qt·tcp/ip
AI软著研究员1 天前
程序员必看:软著不是“面子工程”,是代码的“法律保险”
算法
FunnySaltyFish1 天前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
颜酱1 天前
理解二叉树最近公共祖先(LCA):从基础到变种解析
javascript·后端·算法