算法:强连通分量——tarjan算法

学有向图的连通性不可不学的一个东西就是强连通分量——tarjan算法。

例题

题目描述
给你一张有n个节点,m条边的有向图,求其强连通分量的个数(如果该强连通分量只有一个点则不算在其中)。

输入格式

第1行,两个整数n和m。(n <= 10000,m <= 50000)
第2到(m + 1)行,每行两个整数u和v表示有一条u -> v的边。

输出格式
强连通分量的个数(如果该强连通分量只有一个点则不算在其中)。

输入输出样例

输入

5 4
2 4
3 5
1 2
4 1

输出

1

强连通分量——tarjan算法

再说强连通分量SCC之前你先得了解两个概念:强连通图,其实很好理解,就是这张图中任意两点都可以互相到达(直接或间接);强连通子图,也很好理解,就是一张图中的子图,而这个子图是一个强连通图。那么强连通分量其实就是一张图一个强连通子图,且没有其他能包含这张子图的强连通子图,则称其为一个强连通分量。举个例子,如下图。

在这里插入图片描述

而tarjan算法其实就是把这张图当成一个多叉树,我们任选一个点为根节点,之后按dfs序跑这个树去找环,这里我们要将边分为三类:

  1. 树边:在树上的边。
  2. 返祖边:返回到祖先的边。
  3. 枝杈边:连接两个子树的边,其实没什么用。

其实找强连通分量就是在找环,也就是再找返祖边,那么如何判断一个点到下一个点的边是什么边呢?这个很简单,我们先要维护一个数组dfn[i]表示i节点在树上的dfs序数,这样每次如果说dfn[y] == 0即尚未到过下一个点,那么这一定是一条树边。

接下来我们来判断返祖边和枝杈边,我们可以用一个栈来记录,每到一个点就将该点push到栈中,每找到一个强连通分量就pop掉(后面会详细解释),之后再来一个数组ins[i]表示当前i节点是否在栈中,每次push就将对应点设为true,每次pop就将对应点设为false。这样如果下一个点dfn[y] == 0且ins[i] == true则该边为返祖边,否则为枝杈边。

会判断边了,我们就可以真正开始学习tarjan算法了。这里我们还要维护一个数组low[i]表示i节点及其子节点能直接或间接到达的dfs序数最小的点,这个数组的初值就是i节点自己的dfs序数,之后每次选一条与之相连的边判断:如果这是树边就再去跑其子节点,然后选自己的low和子节点low的最小值为自己的low;如果是返祖边直接选自己的low和返祖点dfs序的最小值为自己的low;如果是枝杈边直接跳过。

这样我们就可以发现如果最后跑完了当前点的所有边,low[x]还等于自己的dfs序数,也就代表这个点跑了一圈还只能到自己的点,那么以这个点为根节点的树上的所有点组成了一个强连通分量。

接下来给这个树上的所有点进行染色,这里我们又可以用到栈了,一边不停地pop,一边取top并将其的颜色染为一个color[0],顺便用cnt数组计个数(cnt[i]表示颜色为i的点的数目),直到当前栈的top等于当前点x了,就停止。

简而言之,其实tarjan算法就是去找返祖边,之后染色计数。这里要注意一下此题的答案是不包含只有一个点的强连通分量的,即cnt[i] > 1时才让ans++。

最后,算一下算法时间复杂度:我们发现需要便利所有边和点一次,所以总时间复杂度大概为O(n + m)级别的。

代码

# include <cstdio>
# include <algorithm>
# include <cmath>
# include <cstring>
# include <vector>
# include <stack>

using namespace std;

const int N_MAX = 10000, M_MAX = 50000;

int n, m;

vector <int> g[N_MAX + 10];

int now, dfn[N_MAX + 10], low[N_MAX + 10];
bool ins[N_MAX + 10];
stack <int> s;

int color[N_MAX + 10], cnt[N_MAX + 10];

void addEdge(int x, int y)
{
	g[x].push_back(y);
}

void tarjan(int x)
{
	dfn[x] = low[x] = ++now;
	ins[x] = true;
	s.push(x);
	for (int i = 0; i < (int) g[x].size(); i++) {
		int y = g[x][i];
		if (dfn[y] == 0) tarjan(y), low[x] = min(low[x], low[y]);
		else if (ins[y]) low[x] = min(low[x], dfn[y]);
	}
	if (low[x] != dfn[x]) return;
	color[x] = ++color[0];
	cnt[color[0]]++;
	while (s.top() != x) {
		int y = s.top();
		color[y] = color[0];
		ins[y] = false;
		s.pop();
		cnt[color[0]]++;
	}
	ins[x] = false;
	s.pop();
}

int main()
{
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= m; i++) {
		int x, y;
		scanf("%d%d", &x, &y);
		addEdge(x, y);
	}
	for (int i = 1; i <= n; i++)
		if (dfn[i] == 0) tarjan(i);
	int ans = 0;
	for (int i = 1; i <= color[0]; i++)
		if (cnt[i] > 1) ans++;
	printf("%d\n", ans);
	return 0;
}
posted @ 2020-03-19 12:12  000紫外线000  阅读(291)  评论(0编辑  收藏  举报