[笔记]tarjan+缩点

[笔记]tarjan+缩点

算法用途:

​ tarjan可以求强连通分量,缩点则是将一个强连通分量缩成一个点

tarjan

概念

​ 1.有向图的强连通分量:再有向图\(G\)中,如果两个顶点\(V_i,V_j\)间有一条从\(V_i\)\(V_j\)的有向路径同时还有一条从\(V_j\)\(V_i\)的有向路径,则称这两个点强连通,在一个图的子图中,任意两点可以相互到达则称这组成了一个强连通分量。

​ 2.一个单独的点也是一个强连通分量。

算法描述

变量:

​ 1.\(dfn\)数组:\(dfn[i]\)表示\(i\)这个点再\(dfs\)时是第几个被搜到的

​ 2.\(low[i]\):表示的是\(i\)这个节点和它的子孙节点中\(dfn\)最小的值

​ 3.\(stack\):表示所有可能构成强连通分量的点,其实就是一个栈

算法过程

一 画图理解

​ tarjan的第一步是对整个图进行\(dfs\),搜完后会得到一棵\(dfs\)树,举个例子.这个树时有向的,显然在这个树上是不会存在环的,因此能产生环的只有可能是一条指向已经访问(搜索)过的边,也就是图中的红边和蓝边。经过观察发现,红边可以产生一个强连通分量,而蓝边不行,因为红边是由\(6\)号节点指向它的祖先\(4\)号节点的,这种红边称为后向边,而蓝边则指向两个没有父子关系的点,这种边称为横叉边,横叉边不一定产生环,而后向边一定产生环。

​ 知道了以上结论后就开始深搜首先会搜到这样的图,此时\(stack = {1,2,3}\),而\(3\)没有多余的指向其它点的边,因此将\(3\)弹出栈,单独作为一个强连通分量,继续深搜,会搜到这样一个图,此时\(stack = {1,2,7}\) 发现节点\(7\)指向已经搜索过的节点\(3\),是上述两种可能存在环的情况,而此时\(3\)不再\(stack\)中,因此不存在环,将\(7,2\)依次弹出栈中,单独作为一个强连通分量,再次深搜,会搜到这样一个图,此时\(stack = {1,4,5,6}\) 发现节点\(6\)有一条连向其他节点的边,即红边,指向的是节点\(4\),而这个点已经搜索过了,符合上面说的产生环的条件,而此时发现节点\(4\)已经在\(stack\)中说明这是一条后向边,可以产生环,因此\(4\)~\(6\)号节点中的所有点组成一个强连通分量。算法结束

​ 但实际情况可能更复杂,这里出现了大环套小环的问题,我们需要对\(dfs\)过程稍作修改(见下)。

二 算法完整步骤

​ 1.首先初始化\(dfn[u] = low[u] = 第几个被搜索到\),但并不是一开始就先跑一遍深搜,而是边做边赋值,具体见代码

​ 2.将当前搜索的节点\(u\)存入\(stack\)

​ 3.遍历每一个与节点\(u\)相连的点,如果遍历到的点的\(dfn\)值为\(0\)则说明这个点之前没有被访问过,那么就对这个没有被访问过的点(假设为\(v\))进行深搜并且更新\(low\)数组的值:low[u] = min(low[u],low[v]),如果与\(u\)相连的点(假设为\(g\))已经被搜索过了,也就是说dfn[g]!=0,此时就更新\(low[u]\)的值:low[u] = min(low[u],dfn[g]),这样就可以保证\(low[u]\)存储的是最先被\(dfs\)到的点,也就保证了找的环是最大的。但为什么是用\(dfn[g]\)来更新呢?因为节点\(g\)可能是另一个强连通分量里的节点,只是还没有出栈,因此节点\(u\)可能不能到达\(low[g]\),但\(u\)一定可以到达\(dfn[g]\)

​ 4.那么什么时候说明找到了一个完整的最大的环呢?当我们找到一个点\(u\)满足low[u] == dfn[u]时,说明这个点的子树中不存在比这个点先搜到的点,则节点\(u\)为它所在的强连通分量里的根节点,所以将\(stack\)\(u\)及它之后的点全部弹出,这就是一个强连通分量。


缩点

算法描述

​ 缩点其实很好理解,也很好实现,只需要新建一个\(col\)数组,用来将同一个连通块内的点染成一个颜色。具体实现看代码。缩完点后的图是一个有向无环图,各个缩完的点由跨越不同强连通分量的边来连接。


例题应用

原题链
题目分析

​ 首先建图,如果\(A\)认为\(B\)受欢迎,则连一条从\(B\)\(A\)的有向边,这样更方便求强连通分量。然后用\(tarjan\)求出所有的强连通分量,再缩点,找到一个出度为\(0\)的缩完后的点,这就是答案,因为缩完点后的图是一个有向无环图,如果一个缩点有出度,则它一定不能被它连出去的那个缩点的牛所喜欢;而一个缩完点后的图中,也不能出现两个没有出度的缩点,因为这样它们就不能被对方缩点里的牛所喜欢,因此我们的答案是只有一个缩点出度为\(0\)的图中那个缩点的大小

代码(tarjan+缩点 & AC code)

#include <bits/stdc++.h>
using namespace std;
struct node{
	int to,next;	
}edge[100010];
int fir[100010],dfn[100010],low[100010],col[100010],num,tot,coln,size[100010],degree[100010];
stack < int > s;
void add(int x,int y){
	tot++;
	edge[tot].to = y;
	edge[tot].next = fir[x];
	fir[x] = tot;
}
void tarjan(int k){
	low[k] = dfn[k] = ++num;
	s.push(k);
	for(int i = fir[k];i;i = edge[i].next){
		int x = edge[i].to;
		if(!dfn[x]){
			tarjan(x);
			low[k] = min(low[k],low[x]);
		}
		else if(!col[x]){//发现后向边 
			low[k] = min(low[k],dfn[x]);
		}
	}
	if(low[k] == dfn[k]){//找到了一个强连通分量的根节点 
		col[k] = ++coln;//缩到一个点里 
		++size[coln];//更新缩点的大小 
		while(s.top() != k){
			col[s.top()] = coln;//缩点 
			++size[coln];//更新缩点的大小 
			s.pop();
		}
		s.pop();//千万不要忘记这一步,要将k节点弹出 
	}
	return;
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i = 1;i <= m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		add(y,x);
	}
	for(int i = 1;i <= n;i++){
		if(!dfn[i])
			tarjan(i); 
	} 
	for(int i = 1;i <= n;i++){
		for(int j = fir[i];j;j = edge[j].next){
			int x = edge[j].to;
			if(col[x] != col[i]){//颜色不同说明不在一个块中,所以出度增加 
				++degree[col[x]];//因为建的是反向边,所以其实是从x连到i 
			}	
		}
	} 
	int flag = 0;//记录有多少出度为0的缩点,如果大于1个则答案为0
	int ans = 0; 
	for(int i = 1;i <= coln;i++){
		if(degree[i] == 0){
			++flag;
			ans = size[i];
		}
	} 
	if(flag != 1){
		printf("0\n");
	}
	else printf("%d\n",ans);
	return 0;
}

完结!

posted @ 2020-09-06 20:16  czyczy  阅读(456)  评论(0编辑  收藏  举报