Tarjan详解

首先,\(tarjan\)是干什么用的?在学之前,我就知道一个名为“缩点”的模板题要用\(tarjan\)算法来解决,所以我对这个算法是这样理解的。把一堆点在不影响题目的情况下缩成一个点,以转化为\(DAG\)(有向无环图)快速求解。其实我觉得模板题正大大体现了\(tarjan\)的优势,就拿模板题来讲一讲这个算法吧。

缩点

思路

这题非常好的体现了\(tarjan\)的优势和用武之处。如果一条边和一个点能无数次重复经过,那么也就是说只要两个点能相互到达,我们就可以把它看做一个点。由于每个点的点权都是正的,那么缩成一个点绝对不会影响结果。正确性很显然,如果你能两个点一起拿再回来,那为什么不拿呢?所以说只要把所有的强联通分量缩成一个点,然后进行拓扑排序\(dp\)即可。

代码

说起来简单,但是代码如何实现呢?以下参考了《算法竞赛进阶指南》:首先从任意一个点开始深搜,搜到出度为\(0\)的节点停止,不重复经过同一个点,这样搜完之后将形成的树称为“搜索树”。还有一个“强联通分量”的定义,按我自己的理解,就是一堆点都能通过它们之间的路径相互到达,那么这些点就构成一个强联通分量,也就是我们要执行缩点的目标。开一个栈,来记录节点之间的父子关系。\(dfn\)表示时间戳,第\(i\)个被访问的点时间戳就是\(i\)\(low\)表示以这个点为子树的根结点,能访问到的最小编号的节点的编号。由于我们是按搜索树编号的,所以很显然,搜索树上一条边的终点时间戳肯定比起点大。所以说,如果两点能相互到达,那么肯定会更新这个点的\(low\),所以说就是一个强联通分量,而且两点中间中间的所有具有父子关系的点都可以相互到达。所以说如果一个点的\(low\)\(dfn\)相等的话,那么它就已经不能和后面的点构成强联通分量了,就只能往前找,去靠拢前面的点。

void tarjan(int x){
	low[x]=dfn[x]=++num;
	sta[++top]=x;
	ins[x]=1;
	for(int i=head[x];i;i=Nxt[i]){
		int y=ver[i];
		if(!dfn[y]){//没有访问过就继续搜
			tarjan(y);
			low[x]=min(low[x],low[y]);//更新low
		}
		else{
			if(ins[y]){//如果有夫子关系,那么也更新
				low[x]=min(low[x],dfn[y]);
			}
		}
	}
	if(low[x]==dfn[x]){//这个点已经到头了,那么往回找
		while(1){
			int y=sta[top];
			ins[y]=0;//一定要出栈,因为它已经跟后面的点构不成强联通分量了
			c[y]=x;
			top--;
			if(x!=y){
				p[x]+=p[y];
			}
			else{
				break;
			}
		}
	}
}

这样\(tarjan\)就差不多了(主要是写给自己看的)

然后再拓扑排序一下,简单\(dp\)就搞定了

完整代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<vector>
#include<queue>
#define ll long long
using namespace std;
const int N=200005,M=400005;
int n,m,tot,top,tc,num,cnt;
ll c[N],head[N],edge[M],Nxt[M],ver[M],low[N],dfn[N],sta[N],p[N],w[N],Nc[M],vc[M],hc[N],f[N],in[N],ans;
bool ins[N];
queue<int> q;
void add(int x,int y){
	ver[++tot]=y;
	Nxt[tot]=head[x];
	head[x]=tot;
}
void add_c(int x,int y){
	vc[++tc]=y;
	Nc[tc]=hc[x];
	hc[x]=tc;
}
void tarjan(int x){
	low[x]=dfn[x]=++num;
	sta[++top]=x;
	ins[x]=1;
	for(int i=head[x];i;i=Nxt[i]){
		int y=ver[i];
		if(!dfn[y]){
			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]){
		while(1){
			int y=sta[top];
			ins[y]=0;
			c[y]=x;
			top--;
			if(x!=y){
				p[x]+=p[y];
			}
			else{
				break;
			}
		}
	}
}
void topo(){
	for(int i=1;i<=n;i++){
		if(!in[i]&&c[i]==i){
			q.push(i);
			f[i]=p[i];
//			printf("%d\n",f[i]);
		}
	}
	while(!q.empty()){
		int x=q.front();
		q.pop();
		for(int i=hc[x];i;i=Nc[i]){
			int y=vc[i];
			in[y]--;
			f[y]=max(f[y],f[x]+p[y]);
			if(in[y]==0){
				q.push(y);
			}
		}
	}
	for(int i=1;i<=n;i++){
		ans=max(ans,f[i]);
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	
	for(int i=1;i<=n;i++){
		scanf("%lld",&p[i]);
	}
	
	for(int i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y);
	}
	
	for(int i=1;i<=n;i++){
		if(!dfn[i]) tarjan(i);
	}
	
	for(int x=1;x<=n;x++){
		for(int i=head[x];i;i=Nxt[i]){
			int y=ver[i];
			if(c[x]==c[y]) continue;
			add_c(c[x],c[y]);
			in[c[y]]++;
		}
	}
	
	topo();
	
	printf("%lld\n",ans);
	return 0;
} 

例题:洛谷P2002 消息扩散

思路

发现这个题貌似比板子题还简单,都不用缩完点之后拓扑排序,缩完点直接看有几个点入度为\(0\)即可,因为入度为\(0\)的点不可能通过别的节点到达。反之,如果入度不为\(0\),那么肯定能到达。

代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<vector>
#include<queue>
#define ll long long
using namespace std;
const int N=100005,M=500005;
int n,m,tot,tc,num,top,ans;
int head[N],ver[M],Nxt[M],c[N],sta[N],dfn[N],low[N],in[N];
bool ins[N];
void add(int x,int y){
	ver[++tot]=y;
	Nxt[tot]=head[x];
	head[x]=tot;
}
void tarjan(int x){
	dfn[x]=low[x]=++num;
	sta[++top]=x;
	ins[x]=1;
	for(int i=head[x];i;i=Nxt[i]){
		int y=ver[i];
		if(!dfn[y]){
			tarjan(y);
			low[x]=min(low[x],low[y]);
		}
		else if(ins[y]){
			low[x]=min(low[x],low[y]);
		}
	}
	if(low[x]==dfn[x]){
		while(1){
			int y=sta[top];
			top--;
			c[y]=x;
			ins[y]=0;
			if(x==y){
				break;
			}
		}
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1,x,y;i<=m;i++){
		scanf("%d%d",&x,&y);
		add(x,y);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]) tarjan(i);
	}
	for(int x=1;x<=n;x++){
		for(int i=head[x];i;i=Nxt[i]){
			int y=ver[i];
			if(c[x]==c[y]) continue;
			in[c[y]]++;
		}
	}
	for(int i=1;i<=n;i++){
		if(!in[i]&&c[i]==i){
			ans++;
		}
	}
	printf("%d\n",ans);
	return 0;
}

例题:洛谷P2341 [USACO03FALL][HAOI2006]受欢迎的牛 G

思路

这个题比上个题稍微多想那么一点点点点。首先如果缩点之后有两个点的出度都为\(0\),那么其中一个点必然不会得到另一个点的“支持”,所以就有\(0\)只受欢迎的牛。反之,只需要看那唯一一个出度为\(0\)的点缩点前内含几个点就行了

代码

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long int ll;
const int N=10005,M=50005;
int n,m,tot,top,num,ans;
int head[N],ver[M],Nxt[M],sta[N],dfn[N],low[N],p[N],c[N],f[N],out[N];
bool ins[N];
queue<int> q;
void add(int x,int y){
	ver[++tot]=y;
	Nxt[tot]=head[x];
	head[x]=tot;
}
void tarjan(int x){
	low[x]=dfn[x]=++num;
	ins[x]=1;
	sta[++top]=x;
	for(int i=head[x];i;i=Nxt[i]){
		int y=ver[i];
		if(!dfn[y]){
			tarjan(y);
			low[x]=min(low[x],low[y]);
		}
		else if(ins[y]){
			low[x]=min(low[x],low[y]);
		}
	}
	if(dfn[x]==low[x]){
		while(1){
			int y=sta[top];
			ins[y]=0;
			top--;
			c[y]=x;
			p[x]++;
			if(x==y) break;
		}
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1,x,y;i<=m;i++){
		scanf("%d%d",&x,&y);
		add(x,y);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]) tarjan(i);
	}
	
	for(int i=1;i<=n;i++){
		if(i==c[i]){
			f[i]=p[i];
		}
	}
	
	for(int x=1;x<=n;x++){
		for(int i=head[x];i;i=Nxt[i]){
			int y=ver[i];
			if(c[x]==c[y]) continue;
			out[c[x]]++;
		}
	}
	int sum=0;
	for(int i=1;i<=n;i++){
		if(i==c[i]&&out[i]==0){
			sum++;
			ans=p[i];
		}
	}
	
	if(sum>1){
		printf("0\n");
		return 0;
	}
	printf("%d\n",ans);
	return 0;
}
posted @ 2020-12-18 16:17  徐明拯  阅读(305)  评论(0编辑  收藏  举报