Tarjan算法

中考前最后一篇博客啦~


之前就一直听说过Tarjan算法(在图论区),但是一直都不知道到底是个什么,应该怎么用,这篇博客就来讲一讲Tarjan算法是个什么东西


Tarjan算法

一些前置知识

1.连通:若在无向图中,任意两个点都可以间接或直接连接,则这个图连通

2.强连通:在有向图中,任意两个点都可以间接或直接连接,则这个图强连通

3.弱连通:刚刚说了强连通是在有向图中,这里指的是在有向图中,把它强行看成是无向图,如果任意两个点都可以间接或直接连接,则这个图弱连通

好了,以上的知识了解就行,并不会太多提及,重点还是强连通分量这个东西。刚刚说了强连通是在整个图中的,那么我们可以在整个图中找到一些小部分的强连通子图,这就叫做强连通分量,画图来理解:

Tarjan算法_第1张图片

其中图①就是一个非强连通图,图②就是一个强连通图

图③中有3个强连通分量,其中{ \({A,B,C,D}\) }为1个强连通分量,{ \({E}\) }为一个强连通分量,{ \({F}\) }

基本概念

这是一个基于DFS深度搜索的算法,我们用一个二元组 \(i \space(xu,low)\) 来表示节点i的一些信息,其中\(xu\)表示\(i\)是第几个被搜索到的,\(low\)表示这个节点最早在什么时候被找到,那么我们模拟一下一张图的过程(因为电脑画图太难啦,但是字迹有点丑,emmm将就一下吧):

Tarjan算法_第2张图片

对于上面的图,我们就可以确定三个强连通分量,但是可能有点描述不清楚

我们对于一个点一直向下找,并存入栈中,记录它的\(xu\)\(low\),并标记这个点已经被寻找过,如果你的下一个点被找过并且还在栈中,那么就可以更新当前这个点的\(low\)。当一个点的\(xu\)\(low\)相等时,说明这是一个强连通分量,就将这一个强连通分量全部从栈中弹出。持续进行这一个操作,直到所有点都被搜过

强烈推荐这个视频,讲得真的非常好,视频模拟以上的过程简单易懂(不是我的)

传送门

int ans;
bool in[MAXN]; //in表示是否在栈中 
int xu[MAXN],low[MAXN]; //搜索的顺序和最开始被发现的时间 
int tim; //记录时间 
int q[MAXN],top; //手动模拟栈 
for(register int i=1;i<=n;i++){
	if(!low[i]) tarjan(i); //对每一个没被找过点进行处理 
}
void tarjan(int s){
	xu[s]=low[s]=++tim;  //先初始化当前点 
	q[++top]=s; //入栈 
	in[s]=true; //在栈中 
	for(register int i=head[s];i;i=e[i].net){
		int y=e[i].to; //找下一个点 
		if(xu[y]==false){ //没找过 
			tarjan(y); //继续 
			low[s]=min(low[s],low[y]); //比较一下 
		}else if(in[y]==true){ //在栈中 
			low[s]=min(low[s],xu[y]); //更新 
		}
	}
	if(low[s]==xu[s]){
		ans++; //强连通分量的数量++ 
		while(q[top]!=s){
			in[q[top]]=false;
			top--;
		}
		in[q[top]]=false;
		top--;//将这个强连通分量全部弹出 
	}
}

这就是一个Tarjan算法的模板吧,但是我并没有找到一个裸的强连通分量的题,但是强连通分量往往会涉及到另外一个东西——缩点

缩点

缩点简单来说,就是将图中的强连通分量全部转化为一个点来表示,这样转化的结果就是将之前的有向有环图转化成了一个有向无环图,而这个被压缩的强连通分量应该存储所有点的信息

如图(是不是生动形象):

Tarjan算法_第3张图片

我们只对强连通分量进行缩点,虽然图中有三个强连通分量,但是因为前面两个孤立的点不太好表示,但是要知道这也是被压缩之后的

那么在Tarjan中已经知道了如何求出每一个强连通分量,也可以非常方便的记录每一个点到底属于哪一个强连通分量,那么将这个强连通分量压缩成点之后,如何还原这个图呢?

其实很简单,对于上面的图,我们有三个强连通分量,那么挨着编个号,在另外用一个邻接表存储就可以了,但是压缩成点之后一定记得记录每一个点的信息

这里就用两到例题直接来讲吧

P3387 【模板】缩点

这道题就是一个非常经典的缩点(不然为什么是模板题),我们将每一个强连通分量压缩为一个点,这个点的点权就是所有在这一个强连通分量中的点的点权之和,然后再在新建的被压缩之后图上跑各种神奇的算法,例如拓扑排序,记忆化搜索等

#include
using namespace std;
const int MAXN=5e5+5;
int n,m;
struct node{
	int net,to,w;
}e[MAXN],e2[MAXN];
int head[MAXN],tot;
void add(int x,int y){
	e[++tot].net=head[x];
	e[tot].to=y;
	head[x]=tot;
}//用来存储原来的图 
int head2[MAXN],tot2;
void add_s(int x,int y){
	e2[++tot2].net=head2[x];
	e2[tot2].to=y;
	head2[x]=tot2;
}//存储压缩之后的图 
int ans;
bool in[MAXN]; 
int xu[MAXN],low[MAXN]; 
int tim;
int q[MAXN],top;
int suo[MAXN],d[MAXN],a[MAXN]; //suo表示这个点属于哪一个强连通分量,d表示这个压缩之后的强连通分量的点权,a就是每个点的点权 
int f[MAXN]; //记忆化搜索 
void tarjan(int s){
	xu[s]=low[s]=++tim;
	q[++top]=s;
	in[s]=true;
	for(register int i=head[s];i;i=e[i].net){
		int y=e[i].to;
		if(xu[y]==false){
			tarjan(y);
			low[s]=min(low[s],low[y]);
		}else if(in[y]==true){
			low[s]=min(low[s],xu[y]);
		}
	}
	if(low[s]==xu[s]){
		ans++;
		while(q[top]!=s){
			in[q[top]]=false;
			suo[q[top]]=ans; //记录属于哪一个强连通分量 
			d[ans]+=a[q[top]]; //权值累计 
			top--;
		}
		suo[q[top]]=ans;
		in[q[top]]=false;
		d[ans]+=a[q[top]];
		top--; 
	}
}
void dfs(int x){
	if(f[x]) return ;
	int sum=0;
	f[x]=d[x];
	for(register int i=head2[x];i;i=e2[i].net){
		int y=e2[i].to;
		dfs(y);
		sum=max(sum,f[y]);
	}
	f[x]+=sum;
} //记忆化搜索记录最大值 
int main(){
	scanf("%d%d",&n,&m);
	for(register int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(register int i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y);
	}
	for(register int i=1;i<=n;i++){
		if(!low[i]) tarjan(i);
	} //找强连通分量 
	for(register int x=1;x<=n;x++){
		for(register int i=head[x];i;i=e[i].net){
			int y=e[i].to;
			if(suo[x]!=suo[y]) add_s(suo[x],suo[y]);
		}
	}//这一坨就是连接将所有强连通分量缩点之后,再进行建图 
	int maxx=0;
	for(register int i=1;i<=ans;i++){
		dfs(i);
		maxx=max(maxx,f[i]);
	}//记录一下最大值 
	printf("%d",maxx);
	return 0;
}

P2341 [USACO03FALL][HAOI2006]受欢迎的牛 G

这就是传说中的爱屋及乌吗

首先对于喜欢的牛,就相当于可以建一条有向边,那么对于一群相互喜欢的牛就是一个强连通分量,然后进行缩点建边,当一个强连通分量出度为0的时候,说明所有的牛都喜欢他们,那么这一个强连通分量中的所有牛都是明星

特别地,当一个图中存在两个及两个以上的出度为0的强连通分量时,说明不可能有一些奶牛被所有人喜欢,这个时候特判答案为0

#include
using namespace std;
const int MAXN=5e5+5;
int n,m;
struct node{
	int net,to,w;
}e[MAXN],e2[MAXN];
int head[MAXN],tot;
void add(int x,int y){
	e[++tot].net=head[x];
	e[tot].to=y;
	head[x]=tot;
}
int head2[MAXN],tot2;
void add_s(int x,int y){
	e2[++tot2].net=head2[x];
	e2[tot2].to=y;
	head2[x]=tot2;
} //还是老样子 
int ans;
bool in[MAXN];
int xu[MAXN],low[MAXN];
int tim;
int q[MAXN],top;
int suo[MAXN],d[MAXN],a[MAXN];
void tarjan(int s){
	xu[s]=low[s]=++tim;
	q[++top]=s;
	in[s]=true;
	for(register int i=head[s];i;i=e[i].net){
		int y=e[i].to;
		if(xu[y]==false){
			tarjan(y);
			low[s]=min(low[s],low[y]);
		}else if(in[y]==true){
			low[s]=min(low[s],xu[y]);
		}
	}
	if(low[s]==xu[s]){
		ans++;
		while(q[top]!=s){
			in[q[top]]=false;
			suo[q[top]]=ans;
			d[ans]+=a[q[top]];
			top--;
		}
		suo[q[top]]=ans;
		in[q[top]]=false;
		d[ans]+=a[q[top]];
		top--;
	}
}//和之前那个缩点的程序一模一样 
int main(){
	scanf("%d%d",&n,&m);
	for(register int i=1;i<=n;i++) a[i]=1; //注意每头牛的权值都为1 
	for(register int i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y);
	}
	for(register int i=1;i<=n;i++){
		if(!low[i]) tarjan(i);
	}
	int maxx=0; 
	int pp[MAXN];
	for(register int x=1;x<=n;x++){
		for(register int i=head[x];i;i=e[i].net){
			int y=e[i].to;
			if(suo[x]!=suo[y]) {
				add_s(suo[x],suo[y]);
				pp[suo[x]]++; //出度增加 
			}
		}
	}
	int kk=0; //记录有多少出度为 0的强连通分量 
	for(register int i=1;i<=ans;i++){
		if(pp[i]==0) maxx+=d[i],kk++; //累加答案 
	}
	if(kk==1)printf("%d",maxx);
	else puts("0");
	return 0;
}

那么Tarjan算法就差不多讲完了,但是Tarjan算法有很多的扩展延伸的空间,可以进行很多操作,但是蒟蒻还没有学习,如果学了会继续更新的,如果还有不懂的,或者是我没有讲到,讲懂的地方,欢迎提问,也可以借助其他dalao的博客进行学习

你可能感兴趣的:(Tarjan算法)