• Tarjan算法


    中考前最后一篇博客啦~


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


    Tarjan算法

    一些前置知识

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

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

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

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

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

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

    基本概念

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

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

    我们对于一个点一直向下找,并存入栈中,记录它的(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中已经知道了如何求出每一个强连通分量,也可以非常方便的记录每一个点到底属于哪一个强连通分量,那么将这个强连通分量压缩成点之后,如何还原这个图呢?

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

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

    P3387 【模板】缩点

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

    #include<bits/stdc++.h>
    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<bits/stdc++.h>
    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的博客进行学习

  • 相关阅读:
    Go语言核心36讲39
    Go语言核心36讲36
    django路飞项目
    设备原理操作
    arp原理实战.docx
    windows10 amd处理器 vmware16pro安装 macOS High Sierra 10.13 显示客户端禁用cpu
    傅里叶变换
    共情的神经生物基础
    网络安全基础
    记录垃圾MySQL的蛋疼失败(相对于mssqlserver)
  • 原文地址:https://www.cnblogs.com/Poetic-Rain/p/13279341.html
Copyright © 2020-2023  润新知