zoukankan      html  css  js  c++  java
  • 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
    ",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
    ",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
    ",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
    ");
    		return 0;
    	}
    	printf("%d
    ",ans);
    	return 0;
    }
    
  • 相关阅读:
    ubuntu16.04安装opencv3.4.0
    基于flask+gunicorn+nginx来部署web App
    ubuntu更换pip install,apt-get,conda install 成国内源
    Ubuntu16.04安装编译caffe以及一些问题记录
    Tutorial for MI5 ! flash MI5 to Native Anriod 9
    在服务器上实现SSH(Single Stage Headless)
    .NET Core容器化开发系列(一)——Docker里面跑个.NET Core
    .NET Core容器化开发系列(零)——计划
    基于.NET Standard的分布式自增ID算法--美团点评LeafSegment
    基于.NET Standard的分布式自增ID算法--Snowflake代码实现
  • 原文地址:https://www.cnblogs.com/57xmz/p/14155539.html
Copyright © 2011-2022 走看看