第四章 图论(5):连通分量

概念

连通性
在无向图 G G G中,若从顶点 v i v_i vi到顶点 v j v_j vj有路径(当然从 v j v_j vj v i v_i vi也一定有路径),则称 v i v_i vi v j v_j vj是连通的。

连通图
在无向图 G G G中,若 V ( G ) V(G) V(G)中任意两个不同的顶点 v i v_i vi v j v_j vj都连通(即有路径),则称 G G G连通图(Con-nected Graph)。
在有向图 G G G中,如果两个顶点 v i v_i vi v j v_j vj间有一条从 v i v_i vi v j v_j vj的有向路径,同时还有一条从 v j v_j vj v i v_i vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图 G G G的任意两个顶点都强连通,称 G G G是一个强连通图

连通分量
无向图的 G G G极大连通子图称为 G G G的连通分量(Connected)。任何连通图的连通分量都只有一个,即使是其本身,非连通的无向图有多个连通分量。
有向图的极大强连通子图,称为强连通分量

一、有向图的强连通分量

强连通分量作用:将有向图通过缩点转换为一个有向无环图(拓扑图、DAG),其中缩点指将所有连通分量表示成一个点。

求强连通分量的方法:DFS。在DFS过程中将树中的边分成四大类:

  1. 树枝边:比如两个点 x 、 y x、y xy,其中 x x x y y y的父结点,两点之间的连线即为树枝边;
  2. 前向边:比如两个点 x 、 y x、y xy,其中 x x x y y y的祖先结点(中间可能间隔多个结点),如果存在一条从 x x x直接指向 y y y的边,则称这条边为前向边(树枝边是一种特殊的前向边);
  3. 后向边:将前向边方向取反,就得到后向边;
  4. 横叉边:从正在搜索的结点指向已经搜索过得结点的边(因为是DFS,所以应该指向左边的结点)。

如何判断一个点是否在强连通分量(SCC)中呢?一个点在强连通分量中:

  1. 可能存在后向边指向祖先结点,使其可以返回祖先结点;
  2. 可能存在横叉边,使其可以沿着横叉边返回搜索过的点,然后走到祖先结点;

因此,如果某个点在某个强连通分量中的话,它一定可以沿着横叉边或者后向边走到其某个祖先结点(前向边不可能构成环)。基于上述启发,有了Tarjan算法求SCC。

引入概念:时间戳,即在dfs过程中对遍历到的结点标记一个从小到大的顺序。

Tarjan算法求SCC
在DFS过程中,对遍历到的点按遍历顺序给定两个时间戳:

  • d f n [ n ] dfn[n] dfn[n]:表示遍历到结点 u u u的时间戳;
  • l o w [ u ] low[u] low[u]:从 u u u开始走,所能遍历到最小时间戳(即以 u u u为根节点的子树中,包含根节点 u u u,所能遍历到最小时间戳)

u u u是其所在的强连通分量的最高点,等价于 d f n [ u ] = = l o w [ u ] dfn[u]==low[u] dfn[u]==low[u],即意味着这个强连通分量不可能走到其它任何一个点,那么就可以找出这个强连通分量。

代码模板

// 栈中存储当前还没有遍历完的强连通分量的所有点
void tarjan(int u)
{
   
	dfn[u] = low[u] = ++ timestamp; // 使二者等于时间戳
	stk[ ++ top] = u, in_stk[u] = true; // 将当前点入栈,并记录当前点是否在栈中
	
	for (int i = h[u]; ~i; i = ne[i]) // 遍历u所有能够到的点
	{
   
		int j = e[i];
		if (!dfn(j)) // 如果当前结点没有被遍历过
		{
   
			tarjan(j);
			low[u] = min(low[u], low[j]); // 用j能够到的点的最小值来更新u的最小值
		}
		else if (in_stk[j]) // 如果当前点依然在栈中
			low[u] = min(low[u], dfn[j]); // 用当前点来更新low值
	}

	if (dfn[u] == low[u])
	{
   
		int y;
		++ scc_cnt;
		do {
   
			y = stk[top -- ];
			in_stk[y] = false;
			id[y] = scc_cnt; // 标记当前点属于哪个强连通分量
		} while (y != u);
	}
}

后续操作:缩点

for int i = 1; i <= n; i ++ 
	for i的所有邻边
		if i和j不在同一SCC中
			加一条边,id(i)id(j)

在做完上述操作之后,这样整张图就变成了DAG图(不用在进行topsort了,因为按连通分量编号递减的顺序一定是拓扑序

1.1 受欢迎的牛

ACwing 1174

如果这个题目的图是一个拓扑图,那么拓扑图最后仅有一个终点的话,即仅有一个出度为零的点,那么终点这头牛一定可以被所有牛欢迎。如果存在两个及以上出度为零的点,这些点之间不可能相互连接,因此不存在一头牛牛被除自己之外的所有牛认为是受欢迎的。

所以可以我们可以先求图中所有强连通分量,然后缩点,将图转化为DAG图,然后观察出度为零的点(缩点前是一个强连通分量,自己一个点也可能是一个强连通分量)里面有多少个点,即有多少头符合结果要求的牛。

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 10010, M = 50010;

int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp; // 时间戳
int stk[N], top;
bool in_stk[N];

// id每个点属于哪个连通分量
// scc_cnt表示图中连通分量个数
// size表示每个连通分量中点的数量
int id[N], scc_cnt, Size[N];

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值