有向图的强连通分量

1.定义

在一个有向图中,如果有两个点 ( u , v ) (u,v) (u,v)可以在图中互相到达,那么就称这两个点是强联通的.。而如果一个有向图上的任意两点都可以互相到达,那么就成这个图为强连通图。而一个有向图中的极大强连通子图就被称为强连通分量。(极大强连通子图就是指一个强连通子图,再加入任何额外的节点都无法保证这个新的图是一个强连通图)

举个例子:
在这里插入图片描述
显然这个图里两个点集 [ 1 , 3 , 5 ] [1,3, 5] [1,3,5] [ 2 , 4 ] [2, 4] [2,4],在各自点集里的各个点都可以互相到达,且不存在更大的点集,所以称这两个点集都是强连通分量。而 [ 1 , 2 ] [1, 2] [1,2] 和[2, 3, 4] 都不可以任意点都互相到达,所以它们不是强连通分量。

2.用处

刚刚介绍了强连通分量的定义,现在来看看强连通分量有什么用吧。
一般强连通分量都是用来缩点的,什么是缩点?就是将一些可以互相到达的点缩成一个点,能够互相到达的点当然就是强连通分量啦。那么缩点的用处是什么呢?显然你会发现在一个有向图中,所有的强连通分量就是一个个的环。你把所有的强连通分量都缩成点之后,再建出来的图,就会建出一个拓扑图(ps. 拓扑图就是有向无环图) 出来。也就是说借助强连通分量你可以将任何有向图都建成一个拓扑图,这样就可以利用很多拓扑图的性质来解题了。比如求有向图的最长/短距离或者要递推某些变量的。
而且求强连通分量的板子基本是不变

3.Tarjan算法求强连通分量

目前Tarjan算法算是比较好理解且好写的求强连通分量的方法,复杂度和维护的性质都比较优秀,Tarjan算法的时间复杂度是线性的 O ( n + m ) O(n+m) O(n+m)

(1)Tarjan的基本流程及简单证明

首先Tarjan解法是基于深度优先搜索进行遍历的算法,我们可以先假设一个dfs遍历节点的过程是类似一颗不会搜索重复节点的搜索树,那么强连通分量就是这棵树上的某几个子树。我们对这颗搜索树的边做几个定义。
在这里插入图片描述

  1. 树枝边:即为按dfs顺序遍历到的所有的边即为树枝边
  2. 前向边:这条边的一个点是另外一个点的祖先,且这条边是由深度低的点指向深度高的点(树枝边就是特殊的前向边)
  3. 后向边:和前向边类似。但是是由深度高的点只想深度低的点。
  4. 横插边:这条边上的两点是两颗子树上的点。且指向一定是由dfs后搜到的点指向先搜到的点

这些定义说实在的和我们讲的Tarjan并没有太大的关系…
Tarjan算法主要是引入了一个新的定义,即时间戳,时间戳说实在的就是dfs遍历的顺序,即dfs序。
Tarjan需要维护两个数组 d f n [ i ] dfn[i] dfn[i] l o w [ i ] low[i] low[i] 以及一个栈。 d f n [ i ] dfn[i] dfn[i] 即为搜索到的第i个节点的时间戳,而 l o w [ i ] low[i] low[i] 得先介绍完栈的用处之后再解释。栈是用来在dfs回溯中,判断某个节点是否在某个还未处理完的强连通分量的。也就是说栈中存储的是按dfs序的顺序存储的还没有处理完的强连通分量的点。一旦判断出某些点是强连通分量后,就会把这些点弹出。 l o w [ i ] low[i] low[i] 这个数组存储的是第 i i i个点包括 i i i点已及还在栈中的 i i i的子树能到达的时间戳最小的点的时间戳。

i i i点以及他的子树全部遍历完了之后,如果 l o w [ i ] low[i] low[i] d f n [ i ] dfn[i] dfn[i] 相等,那么也就是说, i i i这个点是包括这个点的强连通分量时间戳最低的点。那么他自然不可能再加入新的节点来使他的强连通分量变得更大,即包括第 i i i个结点的强连通分量已经搜完,就可以将这个结点从栈中退出来。

值得一提的是,现在栈顶到 i i i个结点之间的所有结点就是这个强连通分量里的结点,所以把栈顶到 i i i个结点之间的所有结点都退出来,他们构成一个强连通分量

证明: 因为首先 i i i结点是这个强连通分量里时间戳最小的一个,且这个过程是在回溯时进行的,故栈中堆顶的元素到 i i i点之间的结点必然是 i i i结点子树上的结点,并且在 i i i点之前的点肯定也不会是 i i i点连通分量里的点(想想 l o w low low 的定义就明白了)。又根据 l o w [ i ] low[i] low[i]的定义,可知这些结点的 l o w low low不可能小于 i i i点的 l o w low low,所以 i i i点子树中不属于 i i i点的连通分量的结点,它们所在的联通分量上的结点也只可能在 i i i点的子树内,而 i i i点的子树已经全部处理完了,所以,在回溯到 i i i点之前,那些不属于 i i i点联通分量的结点必然在之前已经处理完,并退出栈内。所以剩下的点必然都是属于 i i i点联通分量上的结点。(Tarjan牛逼啊)

(2)代码模板

Tarjan算法的模板比较固定,基本上不会做太多的改变,所以一般做题的话,可以直接套板子处理有向图。

补充:一个点就是我看到很多人写完Tarjan后,还会再进行一次拓扑排序,其实完全没有必要,可以注意到,拓扑排序中有一种dfs的写法和Tarjan算法的做法很类似,所以Tarjan后缩点的图的拓扑序,就是这个缩点图的点编号的倒序输出。

vector<int> G[N], D[N]; 
int f[N], g[N];
int dfn[N], low[N], tim, idx, scc_idx; //tim为时间戳,idx为栈顶下标,scc_idx为强连通分量个数
int sta[N], sz[N], id[N];			//sta为栈,sz为缩点后的点里包括多少个点,id为缩点见图后的新下标
bool in_sta[N];						//in_sta为检查某个结点是否还在栈内

void tarjan(int u){
	dfn[u] = low[u] = ++ tim;
	sta[++idx] = u; in_sta[u] = true;
	for(int i = 0;i < G[u].size();i++){
		int v = G[u][i];
		if(!dfn[v]){
			tarjan(v);
			low[u] = min(low[u],low[v]);
		}
		else if(in_sta[v]) low[u] = min(low[u],low[v]);
	}
	if(low[u] == dfn[u]){
		scc_idx++;
		int y;
		do{
			y = sta[idx--];
			in_sta[y] = false;
			id[y] = scc_idx;
			sz[scc_idx]++;
		}while(y != u);
	}
}

(3)例题

a. USACO 2003 Fall (受欢迎的牛)
题目描述

每一头牛的愿望就是变成一头最受欢迎的牛。现在有 N 头牛,给你 M 对整数 (A,B),表示牛 A 认为牛 B 受欢迎。这种关系是具有传递性的,如果 A 认为 B 受欢迎,B 认为 C 受欢迎,那么牛 A 也认为牛 C 受欢迎。你的任务是求出有多少头牛被除自己之外的所有牛认为是受欢迎的。

输入

第一行两个数 N,M;

接下来 M 行,每行两个数 A,B,意思是 A 认为 B 是受欢迎的(给出的信息有可能重复,即有可能出现多个 A,B)。

输出

输出被除自己之外的所有牛认为是受欢迎的牛的数量。

输入样例

3 3
1 2
2 1
2 3

输出样例

1

数据范围:

对于全部数据, 1 ≤ N ≤ 1 0 4 , 1 ≤ M ≤ 5 × 1 0 4 1≤N≤10^4,1≤M≤5×10^4 1N104,1M5×104

题解

对于这道题没啥好说的,缩完点直接出度为0的点有多少,超过1个的答案就是0。否则就是那个点的大小(大小就是缩点前的这个强连通分量包括几个点)

代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+50;

vector<int> G[N];
int tim, sta[N], sz[N], id[N], idx; 
int scc_idx, dfn[N], low[N], du[N];
bool in_sta[N];

void tarjan(int u){
	dfn[u] = low[u] = ++tim;
	sta[++idx] = u; in_sta[u] = true;
	for(int i = 0;i < G[u].size();i++){
		int v = G[u][i];
		if(!dfn[v]){
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}
		else if(in_sta[v]){
			low[u] = min(low[u], low[v]);
		}
	}
	if(low[u] == dfn[u]){
		int y;
		scc_idx++;
		do{
			y = sta[idx--];
			in_sta[y] = false;
			id[y] = scc_idx;
			sz[scc_idx]++;
		}while(u != y);
	}
}

int main(){
	int n, m;
	scanf("%d %d",&n,&m);
	while(m--){
		int a, b;
		scanf("%d %d",&a,&b);
		G[a].push_back(b);
	}
	for(int i = 1;i <= n;i++){
		if(!dfn[i]) tarjan(i);
	}
	for(int i = 1;i <= n;i++){
		for(int j = 0;j < G[i].size();j++){
			if(id[G[i][j]] != id[i]) du[id[i]]++;
		}
	}
	int ans = 0, tmp = 0;
	for(int i = 1;i <= scc_idx;i++){
		if(du[i] == 0) ans = sz[i], tmp++;
	}
	if(tmp > 1) printf("0\n");
	else printf("%d\n",ans);
	return 0;
}
b.《算法竞赛进阶指南》, usaco training 5.3 (学校网络)
题目描述

一些学校连接在一个计算机网络上,学校之间存在软件支援协议,每个学校都有它应支援的学校名单(学校 A 支援学校 B,并不表示学校 B 一定要支援学校 A)。

当某校获得一个新软件时,无论是直接获得还是通过网络获得,该校都应立即将这个软件通过网络传送给它应支援的学校。

因此,一个新软件若想让所有学校都能使用,只需将其提供给一些学校即可。

现在请问最少需要将一个新软件直接提供给多少个学校,才能使软件能够通过网络被传送到所有学校?

最少需要添加几条新的支援关系,使得将一个新软件提供给任何一个学校,其他所有学校就都可以通过网络获得该软件?

输入格式

第 1 行包含整数 N,表示学校数量。

第 2…N+1 行,每行包含一个或多个整数,第 i+1 行表示学校 i 应该支援的学校名单,每行最后都有一个 0 表示名单结束(只有一个 0 即表示该学校没有需要支援的学校)。

输出格式

输出两个问题的结果,每个结果占一行。

数据范围

2≤N≤100

输入样例:

5
2 4 3 0
4 5 0
0
0
1 0

输出样例:

1
2

题解

这个题目第一问很简单,就是缩点后入读为0的点的个数。
第二问的答案则是入读为0的点和出度为0的点取最大值。
这边只证明第二问:假设有 p p p和入读为0的点, q q q个初读为0的点,这边先假设 ( p < = q ) (p <= q) (p<=q) ( p > q ) (p > q) (p>q) 的证明类似。
我们称出度为0的点为终点,入读为0的点为起点
首先可以肯定地是一个终点总有一个起点可以到达它。
我们先看 p = 1 p = 1 p=1的时候

如果 p = 1 p = 1 p=1 那么也就是说这个起点可以到达所有的终点,即只要让每个终点都和起点连一个边即可即 q q q条边。

如果 p > 1 p > 1 p>1 那么必然可以找到两组不同的起点 ( q 1 , q 2 ) (q1,q2) (q1,q2)走到不同的终点 ( p 1 , p 2 ) (p1,p2) (p1,p2)。 即 ( q 1 − > p 1 ) (q1->p1) (q1>p1) ( q 2 − > p 2 ) (q2->p2) (q2>p2)。于是我们总可以建一个 ( p 1 − > q 2 ) (p1->q2) (p1>q2) 的边使得起点和终点各减少一个。最后会变为 q = 1 q = 1 q=1 的情况。所以发现我们总可以只建 m a x ( p , q ) max(p,q) max(p,q)个边使得起成为整个图变成强连通分量。并且一定是最优的。

然后在特判一下,如果整个图只有一个强连通分量,那么第二问答案就是0。

代码
//强联通分量可以把任意有向图转化为一个有向无环图(拓扑图),并且使用tarjan 算法求出的强连通分量的编号逆向输出就是这个拓扑图的拓扑序 
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+50;

vector<int> G[N];
bool in_sta[N];
int id[N], low[N], dfn[N], sta[N];
int tim, in[N], out[N], scc_idx, idx; 

void tarjan(int u){
	dfn[u] = low[u] = ++tim;
	sta[++idx] = u; in_sta[u] = true;
	for(int i = 0;i < G[u].size();i++){
		int v = G[u][i];
		if(!dfn[v]){
			tarjan(v);
			low[u] = min(low[u], low[v]); 
		}
		else if(in_sta[v]) low[u] = min(low[u],dfn[v]);
	}
	if(low[u] == dfn[u]){
		int y;
		scc_idx++;
		do{
			y = sta[idx--];
			in_sta[y] = false;
			id[y] = scc_idx;
		}while(y != u);
	}
}

int main(){
	int n;
	scanf("%d",&n);
	int ans1 = 0, ans2 = 0;
	for(int i = 1;i <= n;i++){
		int x;
		while(scanf("%d",&x) && x != 0) G[i].push_back(x);
	}
	for(int i = 1;i <= n;i++){
		if(!dfn[i]) tarjan(i);
	}
	for(int i = 1;i <= n;i++){
		for(int j = 0;j < G[i].size();j++){
			int v = id[G[i][j]];
			if(v != id[i]) {
				in[v]++; out[id[i]]++;
			}
		}
	}
	for(int i = 1;i <= scc_idx;i++){
		if(in[i] == 0) ans1++;
 		if(out[i] == 0) ans2++;
	}
	printf("%d\n",ans1);
	if(scc_idx == 1) printf("0\n");
	else printf("%d\n",max(ans1,ans2));
	return 0;
}
/*
5
2 4 3 0
4 5 0
0
0
1 0
*/
  • 8
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值