有向图的强连通分量

本文详细介绍了图论中的强连通分量概念,包括强连通图的定义以及如何寻找强连通分量。文章通过 Tarjan 算法来演示如何在有向图中找到强连通分量,并给出了算法的实现细节。同时,针对题目要求,提供了输出指定节点所在强连通分量的解决方案,确保每个分量只输出一次,并按节点编号排序。代码示例使用 C++ 编写,实现了一种高效的强连通分量查找算法。
摘要由CSDN通过智能技术生成

B3609 [图论与代数结构 701] 强连通分量

一些概念

  1. 若一张有向图中任意两个节点 x , y x,y x,y,存在 x x x y y y 的路径和 y y y x x x 的路径,则称其为强连通图
  2. 有向图的极大强连通子图被称为强连通分量

在上文中,一个强连通子图 G ′ = ( V ′ , E ′ ) ( V ⊆ V , E ′ ⊆ E ) G'=(V',E')(V\subseteq V,E'\subseteq E) G=(V,E)(VV,EE) 极大,当且仅当不存在包含 G ′ G' G 的更大子图 G ′ ′ = ( V ′ ′ , E ′ ′ ) G''=(V'',E'') G=(V,E) 满足 V ′ ⊆ V ′ ′ ⊆ V , E ′ ⊆ E ′ ′ ⊆ E V'\subseteq V''\subseteq V,E'\subseteq E''\subseteq E VVV,EEE。显然,一个环一定是强连通图,所以我们的思路就是找到能和某个点构成环的所有点。

正如本文的 URL,强连通分量简记为 SCC(Strongly Connected Component) \text{SCC(Strongly Connected Component)} SCC(Strongly Connected Component)

SCC \text{SCC} SCC 可以用 Tarjan,Kosaraju 或者 Garbow 算法,本文使用 Tarjan 算法。

定义

  • ( int ⁡ \operatorname{int} int) T i m e Time Time:当前时间戳;
  • ( int ⁡ \operatorname{int} int) t o t tot tot SCC \text{SCC} SCC 的个数;
  • ( int ⁡ \operatorname{int} int) d f n ( u ) dfn(u) dfn(u):点 u u u 的 dfs 序;
  • ( int ⁡ \operatorname{int} int) l o w ( u ) low(u) low(u):以下节点的 d f n dfn dfn 的最小值: v ∈ s u b t r e e ( u ) v\in subtree(u) vsubtree(u) u u u 的子树) 及从 v v v 出发通过一条不在搜索树上的边(非树边)能到达的节点;
  • ( int ⁡ \operatorname{int} int) c ( u ) c(u) c(u):记录点 u u u 所在的 SCC \text{SCC} SCC
  • ( stack ⁡ \operatorname{stack} stack< int ⁡ \operatorname{int} int>) s t a sta sta:一个栈;
  • ( bool ⁡ \operatorname{bool} bool) i n s ( u ) ins(u) ins(u):点 u u u 是否在 s t a sta sta 中;
  • ( vector ⁡ \operatorname{vector} vector< int ⁡ \operatorname{int} int>) s c c ( i ) scc(i) scc(i):记录编号为 i i i SCC \text{SCC} SCC 内的所有节点。

Tarjan 算法使用 dfs 实现:

  1. 记录 d f n , l o w dfn,low dfn,low
  2. 当前节点进栈;
  3. 更新 l o w low low
    1. d f n ( v ) = 0 dfn(v)=0 dfn(v)=0:说明 v v v u u u 的直系儿子, v v v 能到的 u u u 都能到,先往下递归,再直接用 l o w ( v ) low(v) low(v) 来更新 l o w ( u ) low(u) low(u)
    2. i n s ( v ) = t r u e ins(v)=true ins(v)=true:说明 v v v u u u 的祖先且 u u u 可以通过一条非树边到达 v v v,根据定义可以用 d f n ( v ) dfn(v) dfn(v) 来更新 l o w ( u ) low(u) low(u)
  4. 更新后,如果 d f n ( u ) = l o w ( u ) dfn(u)=low(u) dfn(u)=low(u),说明从 u u u 出发最终又能回到 u u u,即构成了环,是一个满足要求的 SCC \text{SCC} SCC。将 t o t ← t o t + 1 tot\gets tot+1 tottot+1,同时不停地弹栈直到弹到 u u u,则弹出的所有节点都在 s u b t r e e ( u ) subtree(u) subtree(u) 内;记录其在第 t o t tot tot SCC \text{SCC} SCC 内;最后将其加入 s c c ( t o t ) scc(tot) scc(tot) 中。

对于题目的特殊要求要求:

第一行输出 1 1 1 号点所在强连通分量,第二行输出 2 2 2 号点所在强连通分量,若已被输出,则改为输出 3 3 3 号点所在强连通分量,以此类推。

开一个 ( bool ⁡ \operatorname{bool} bool) v i s vis vis 数组记录每个 SCC \text{SCC} SCC 是否已经输出过即可。

每个强连通分量按节点编号大小输出

sort ⁡ \operatorname{sort} sort 一遍即可。


Code \text{Code} Code

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <stack>
#include <vector>
#define re register
using namespace std;

inline int read()
{
	re int x = 0, f = 0;
	re char c = getchar();
	while (c < '0' || c > '9')
	{
		f |= c == '-';
		c = getchar();
	}
	while (c >= '0' && c <= '9')
	{
		x = (x << 3) + (x << 1) + (c ^ '0');
		c = getchar();
	}
	return f ? -x : x;
}

inline void write(int x)
{
	if (x < 0)
	{
		putchar('-');
		x = -x;
	}
	if (x > 9)
	{
		write(x / 10);
	}
	putchar(x % 10 ^ '0');
}

inline int min2(int x, int y)
{
	return x < y ? x : y;
}
//-----------------------------------------------------------
const int MAXN = 1e4 + 5;
const int MAXM = 1e5 + 5;

int cnt, Time, tot;
int head[MAXN], dfn[MAXN], low[MAXN], c[MAXN];
bool ins[MAXN], vis[MAXN];
stack<int> sta;
vector<int> scc[MAXN];

struct edge
{
	int to, nxt;
}e[MAXM];

void add(int u, int v)
{
	e[++cnt] = edge{v, head[u]};
	head[u] = cnt;
}

void tarjan(int u)
{
	dfn[u] = low[u] = ++Time; //初始化
	sta.push(u); //进栈
	ins[u] = true; //标记
	for (re int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].to; //更新
		if (!dfn[v])
		{
			tarjan(v);
			low[u] = min2(low[u], low[v]);
		}
		else if (ins[v])
		{
			low[u] = min2(low[u], dfn[v]);
		}
	}
	if (dfn[u] == low[u]) //构成了环
	{
		tot++;
		int v = 0;
		while (u != v)
		{
			v = sta.top(); //弹栈
			sta.pop();
			ins[v] = false; //取消标记
			c[v] = tot;
			scc[tot].push_back(v); //记录答案
		}
	}
}

int main()
{
	int n = read(), m = read();
	for (re int i = 1; i <= m; i++)
	{
		int u = read(), v = read();
		add(u, v);
	}
	for (re int i = 1; i <= n; i++)
	{
		if (!dfn[i]) //防止不连通
		{
			tarjan(i);
		}
	}
	write(tot);
	putchar('\n');
	for (re int i = 1; i <= n; i++)
	{
		int x = c[i];
		if (vis[x])
		{
			continue;
		}
		vis[x] = true; //已输出过
		sort(scc[x].begin(), scc[x].end());
		for (re int i = 0; i < scc[x].size(); i++)
		{
			write(scc[x][i]);
			putchar(' ');
		}
		putchar('\n');
	}
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值