首页 > 其他分享 >Tarjan的学习笔记

Tarjan的学习笔记

时间:2023-12-26 22:44:57浏览次数:33  
标签:Tarjan int tarjan 笔记 学习 dfn low head 节点

\(Tarjan\)的学习笔记

一,\(tarjan\)概述:


(1)定义:

$~~~~~~~~$$tarjan$是基于深度优先搜索的一种算法,求解图的连通性等问题,巧妙地利用了对图进行深搜时产生的搜索树上的边。

(2)\(tarjan\)中的几种边:

\(~~~~~~~~\)树边:父亲与孩子的边。

\(~~~~~~~~\)前向边:祖先到孩子的边(树边属于特殊的前向边)。

\(~~~~~~~~\)后向边:孩子到祖先的边(与前向边相反)。

\(~~~~~~~~\)横叉边:这种边只有在有向图时才有,两个在搜索树上无祖宗、子孙关系的点之间的边。

\(~~~~~~~~\)在\(tarjan\)中,我们只关心树边和后向边


二,\(tarjan\)应用一:求解有向图的强联通分量(\(SCC\))(然后可以缩点)


(1)有向图的强联通分量(\(SCC\))的定义:

\(~~~~~~~~\)在有向图中,如果两个顶点\(u\),\(v\)存在\(u→v\)和\(v→u\),那么则称\(u\),\(v\)强连通。

\(~~~~~~~~\)如果有向图的任意两个顶点都强连通,则称为强连通图。

\(~~~~~~~~\)有向非强连通图的极大强连通子图,称为强连通分量。


(2)算法思路:

\(~~~~~~~~\)求解有向图的强联通分量时,最重要的是后向边,因为如果存在后向边\(u→v\)(根据定义,此时一定有\(v→u\),为树枝边构成),那么\(u\)、\(v\)一定在同一个强连通分量中,那么,如何求解一条边是否为后向边呢?

\(~~~~~~~~\)首先,我们定义一个数组\(dfn\),表示第\(u\)个节点是第几个被遍历到的(即时间戳)

\(~~~~~~~~\)接着,我们可以定义一个栈\(s\),其中的元素为当前搜索树上的点,那么如果存在一条后向边\(u→v\),那么此时\(v\)一定在栈中。

\(~~~~~~~~\)于是我们再定义一个\(vis\)数组,就可以轻松找到\(v\)是否在栈里面了(入栈时\(vis\)变为\(1\),出栈时\(vis\)变为\(0\))这样子我们就可以找到两个点是否为强联通的了

\(~~~~~~~~\)那么,怎么把一个\(SCC\)的所有点都找出来呢?

\(~~~~~~~~\)我们可以定义一个新的数组,\(low\)。\(low[u]\)表示从\(u\)出发最多只通过一条后向边能回溯到最小的\(dfn\)值。(注意,是从\(u\)出发,也就是说是从\(u\)以及它的子树回溯的最早的\(dfn\)值)

\(~~~~~~~~\)显然,若\(u→v\)是一个后向边,那么\(v\)是\(u\)在搜索树上的祖先,那么,\(low[u]=min(low[u],dfn[v])\)。而对于\(v→u\)路径上的每一个节点\(w\),\(w\)和\(v\)、\(u\)属于同一个\(SCC\),那么\(low[w]=min(low[w],dfn[v])\)。


\(~~~~~~~~\)那么,怎么用代码实现以上的步骤,对\(low\)数组进行更新呢?

\(~~~~~~~~\)我们可以在\(dfs\)内进行回溯时,判断一下当前搜索的节点\(u\)能到达的节点\(v\)中,是否有\(v\)也在栈中,如果在,那么对\(low\)数组进行更新,即\(low[u] = min(low[u], dfn[v])\)(此时\(u→v\)为后向边)。

\(~~~~~~~~\)另外,如果\(u→v\)为一条树边,那么\(low[u]=min(low[u],low[v])\)

\(~~~~~~~~\)而在\(u\)所有的子树搜索完后,若\(low[u]\)依旧等于\(dfn[u]\),则意味着节点\(u\)为它所在的\(SCC\)中第一个搜索到的节点,它无法回溯到\(dfn\)值比它小的节点,那么此时在栈中比\(u\)先出栈的元素一定就是和\(u\)在同一\(SCC\)中的。

\(~~~~~~~~\)那么,将栈中的比\(u\)先出栈的元素和\(u\)依次出栈,再用一个数组\(color\)对其进行标记(表示同属一个\(SCC\)),即可。

\(~~~~~~~~\)如果要进行缩点的话,那么所有\(color\)数组值一样的点可以看作一个大点,缩点后可以重新建图,或进行其他操作。

\(~~~~~~~~\)时间复杂度:\(O(N+M)\),每个点出栈一次,进栈一次,每条边被访问一次。



(3)代码+注释:
#include<bits/stdc++.h>
using namespace std;
const int N=10000+9,M=10000+9;
stack<int> s;
int dfn[N],low[N],head[N],f[N];
bool vis[N];
int color[N];
int tot,at,sum;
struct node{
    int to,nt;
}a[M];
int n,m;
void add(int x,int y){
	a[++at].to=y,a[at].nt=head[x],head[x]=at;
}
void tarjan(int u){
    dfn[u]=low[u]=++tot;
    vis[u]=1;
    s.push(u);
    for(int i=head[u];i;i=a[i].nt){
        int v=a[i].to;
        if(!dfn[v]){//树边
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(vis[v]){//后向边
            low[u]=min(low[u],dfn[v]);
        }
    }
    if(dfn[u]==low[u])//u为它所在的SCC中第一个搜索到的节点
    {
        sum++;
        while(1){
            int now=s.top();
            s.pop();
            vis[now]=0;
            color[now]=sum;
            if(now==u) break;
        }//依次出栈,进行标记
    }
}

int main(){
    int x,y;
    scanf("%d%d",&n,&m);
    for (int i=1;i<=m;i++){
        scanf("%d%d",&x,&y);
        add(x,y);
    }
    for(int i=1;i<=n;i++){//如果当前节点未被搜索,则从此节点开始搜索
        if (!dfn[i])tarjan(i);
    }
    return 0;
}



三,\(tarjan\)应用二:求解无向图的割点

(1)无向图的割点的定义:

\(~~~~~~~~\)若删除一个点(以及和它关联的边)后,原先的图分裂成两个及以上的不相连的图,则称那个点为割点。



(2)算法思路:

\(~~~~~~~~\)首先,我们思考一下,在搜索树上,一个点满足什么条件的时为割点?

\(~~~~~~~~\)①如果这个点为搜索树的根节点,那么若有两个及以上的子树无法到达除这个点外的点,那么把这个点删掉后,就会产生多个不相连的图。


\(~~~~~~~~\)②如果这个点不为搜索树的根节点,那么只要有一个子树无法到达除这个点外的点,那么把这个点删掉后,就会产生不相连的图。


\(~~~~~~~~\)也就是说,在进行搜索时,如果有\(u→v\),并且\(v\)没有被搜索过,那么对\(v\)进行搜索。

\(~~~~~~~~\)回溯后,若\(low[v]<=dfn[u]\),那么就代表节点\(v\)为根节点的搜索树上的所有点都无法回溯到时间戳比\(u\)跟小的点,则代表删除\(u\)后,这个子树间不能同除了\(u\)和这个子树上的点以外的点相连,若符合上述条件,则\(u\)为割点。


\(~~~~~~~~\)那么,具体的\(tarjan\)代码只需要在求\(SCC\)的代码上进行一些小修改即可

\(~~~~~~~~\)因为我们只关心一个点能回溯到的\(dfn\)的最小值,那么在搜索到\(u\)时,若存在\(u→v\),且\(v\)没有被搜索过,那么就按照上文说的进行,否则则对\(low[u]\)值进行更新:\(low[u]=min(low[u],dfn[v])\)。



(3)代码+注释:
#include<bits/stdc++.h>
using namespace std;
const int N=10000+9,M=10000+9;
int n,m;
struct node{
	int to,nt;
}a[M*2];
int head[N],at;
void add(int x,int y){
	a[++at].to=y,a[at].nt=head[x],head[x]=at;
}

int dfn[N],low[N];
int num=0;
int root;
bool vis[N];
void tarjan(int u){
	dfn[u]=low[u]=++num;
	int v;
	int flag=0;
	for(int i=head[u];i;i=a[i].nt){
		v=a[i].to;
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
			if(dfn[u]<=low[v]){
				flag++;
				if(u!=root||flag>=2) vis[u]=1;//判断是否符合条件
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
}

int main(){
	scanf("%d%d",&n,&m);
	int x,y;
	for(int i=1;i<=m;i++){
		scanf("%d%d",&x,&y);
		add(x,y);
		add(y,x);
	}

	for(int i=1;i<=n;i++){
		if(!dfn[i]){
			root=i;
			tarjan(i);
		}
	}

	return 0;
}


四,\(tarjan\)应用三:求解无向图的割边(桥)

(1)无向图的割边(桥)的定义:

\(~~~~~~~~\)若删除这条边后,原先的图分裂成两个及以上的不相连的图,则称这条边为割边(也称为桥)。



(2)算法思路:

\(~~~~~~~~\)首先,我们思考一下,在搜索树上,一个点满足什么条件的时为桥?

\(~~~~~~~~\)其实和割点需要满足的条件差不多,首先,一个桥在搜索树上一定是一条树边,不然的话割掉它一定不会对图的连通性产生影响

\(~~~~~~~~\)但是,显然的是,肯定不是所有树边都为桥,只有在\(u→v\),并且\(low[v]<dfn[u]\)时才成立,因为这样的话,在搜索树上,以\(v\)为根的子树能通过一条后向边回溯到的点就是在这棵子树上的,把这条边删除后它们就独立出来了。(注意,是\(<\),而不是\(<=\),因为我们删除的是边)

\(~~~~~~~~\)不过需要注意的是,因为是无向图,但我们采用链式前向星存图的时候一条边是存为两条有向边的,所以有可能会产生将走过的树边变为后向边情况,这样子就无法求解了。

\(~~~~~~~~\)不过由于存图相同的边是挨着的,且下标是一奇一偶,因此在使用\(v\)数组标记割边时,我们应该标记\(i\)和\(i\wedge1\)(因为奇数异或\(1\)为那个数\(+1\),偶数为那个数\(-1\)),这样也可以避免把重边一刀切的情况。

\(~~~~~~~~\)同理,在走后向边时,也要判断一下,不是的话再更新\(low\)数组,否则则是在走做过的边。所以在\(tarjan\)时还要传一个参,表示它是由经过条边达到的,而在初始调用时则传参\(0\),所以储存图的链表数组下标从\(2\)开始。(\(0\wedge1=1\))



(3)代码+注释:
#include<bits/stdc++.h>
using namespace std;
const int N=10000+9,M=10000+9;
int n,m;
struct node{
	int to,nt;
}a[M*2];
int head[N],at=1;//从1开始
void add(int x,int y){
	a[++at].to=y,a[at].nt=head[x],head[x]=at;
}
bool vis[N];
int dfn[N],low[N];
int num=0;
void tarjan(int x,int k){
	dfn[x]=low[x]=++num;
	int y;
	for(int i=head[x];i;i=a[i].nt){
		y=a[i].to;
		if(!dfn[y]){
			tarjan(y,i);
			low[x]=min(low[x],low[y]);
			if(dfn[x]<low[y]){
                vis[i]=vis[i^1]=1;
			}
		}
		else if(i!=(k^1)) low[x]=min(low[x],dfn[y]);//判断是否走过
	}
}

int main(){
	scanf("%d%d",&n,&m);
	int x,y;
	for(int i=1;i<=m;i++){
		scanf("%d%d",&x,&y);
		add(x,y);
        add(y,x);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]){
			tarjan(i,0);
		}
	}
	return 0;
}



五,\(tarjan\)应用四:求解无向图的点双连通分量(\(v-DCC\))(然后可以缩点)

(1)无向图的点双连通分量(\(v-DCC\))的定义:

\(~~~~~~~~\)若一个无向图不存在割点,称它为“点双连通图”,即删除任意一个点都连通。

\(~~~~~~~~\)无向图的极大点双连通子图称为“点双连通分量”,简称\(v-DCC\)。



(2)算法思路:

\(~~~~~~~~\)我们可以在求解割点的时候,同时求解点双连通分量(\(v-DCC\)),而过程基本同求解割点时一致,不过要注意的是,割点同时属于多个\(v-DCC\)

\(~~~~~~~~~\)我们同求解\(SCC\)时一样,维护一个栈,在第一次访问节点\(u\)时,将其入栈。

\(~~~~~~~~~\)在判定割点时,若有\(u→v\),使得\(u\)为割点,从则栈顶不断弹出节点,直至\(u\)在栈中的上一个节点,也就是\(v\)被弹出,(割点同时属于多个\(v-DCC\),不用弹出)而弹出的所有节点与节点\(u\)一起构成一个点双连通分量

\(~~~~~~~~\)不过,同求割点不同的是,求解只要\(v-DCC\)时,只要是满足了\(dfn[u]<=low[v]\),就代表找到了一个\(v-DCC\)。为什么呢?

\(~~~~~~~~\)首先,求割点时的特判是针对\(u\)为根节点的情况的,那么我们分类讨论一下:①在搜索时,只有一次满足了这个条件,那么代表此时搜索树上的节点同属一个\(v-DCC\)。②不止一次满足了,那么\(u\)就是割点,那么此时访问到的节点就是同属一个\(v-DCC\)的。

\(~~~~~~~~\)注意,如果\(u\)不与任何点相连,那么它独属于一个\(v-DCC\),在\(tarjan\)开始时特判即可。

\(~~~~~~~~\)最后,如果要进行缩点的话,我们将同属一个\(v-DCC\)的点加入同一个\(vector\)数组中,即可。



(3)代码+注释:
#include<bits/stdc++.h>
using namespace std;
const int N=10000+9,M=10000+9;
int n,m;
struct node{
	int to,nt;
}a[M*2];
int head[N],at;
inline void add(int x,int y){
	a[++at].nt=head[x],head[x]=at,a[at].to=y;
}
int dfn[N],low[N];
vector<int> b[N];//储存同属一个v-DCC的点
stack<int> q;
int num=0;
int bt,root;
void tarjan(int u){
	dfn[u]=low[u]=++num;
	q.push(u);
	if(u==root&&head[u]==0){//特判不与任何点联通的情况
		b[++bt].push_back(u);
		return;
	}
	int v;
	for(int i=head[u];i;i=a[i].nt){
		v=a[i].to;
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
			if(dfn[u]<=low[v]){
				bt++;
				int x;
				do{
					x=q.top();q.pop();
					b[bt].push_back(x);
				}while(x!=v);
				b[bt].push_back(u);
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
}

int main(){
	scanf("%d%d",&n,&m);
	int x,y;
	for(int i=1;i<=m;i++){
		scanf("%d%d",&x,&y);
		add(x,y),add(y,x);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]){
			root=i;
			tarjan(i);
		}
	}
	return 0;
}


六,\(tarjan\)应用五:求解无向图边双连通分量(\(e-DCC\))(然后可以进行缩点)

(1)无向图边双连通分量(\(e-DCC\))的定义:

\(~~~~~~~~\)若一个无向图不存在桥(割边),称它为“边双连通图”,即删除任意一条边都连通。

\(~~~~~~~~\)无向图的极大边双连通子图称为“边双连通分量”。(\(e-DCC\))



(2)算法思路:

\(~~~~~~~~\)首先,求解的过程基本上同求解割边(桥)的过程差不多,还是老样子,我们用一个栈储存当前已经遍历到且没有找到所在的\(e-DCC\)的点

\(~~~~~~~~\)我们想一想,如果有\(u-v\)的边为割边且\(u→v\)为树边,就是意味着此时\(v\)节点通过除了这条割边外的边访问不到不在它子树内的点,那么以\(v\)为根节点的子树就同属一个\(e-DCC\),而此时\(dfn[v]=low[v]\)

\(~~~~~~~~\)那么,就和求解\(SCC\)时一样,在\(tarjan\)的末尾加上一个判断,若符合条件就将栈中在\(v\)前面的节点和\(v\)出栈,这些节点就同属于一个\(e-DCC\)。

\(~~~~~~~~\)若要进行缩点的话,和之前一样,用数组\(color\)将出栈的元素标记就可以了。



(3)代码+注释:
#include<bits/stdc++.h>
using namespace std;
const int N=5001,M=10001;
int n,m;
struct node{
	int to,nt;
}a[M*2];
int head[N],at=1;//注意,从1开始
void add(int x,int y){
	a[++at].nt=head[x],head[x]=at,a[at].to=y;
}
int dfn[N],low[N];
bool vis[M*2+100],v[N];
int color[N];
int num=0;
stack<int> q;
int tot;
void tarjan(int u,int k){
	dfn[u]=low[u]=++num;
	q.push(u);
	int v;
	for(int i=head[u];i;i=a[i].nt){
		v=a[i].to;
		if(!dfn[v]){
			tarjan(v,i);
			low[u]=min(low[u],low[v]);
		}
		else if(i!=(k^1)) low[u]=min(low[u],dfn[v]);
	}
    //同求解割边一样
	if(low[u]==dfn[u]){//判断是否符合条件
		tot++;
		do{
			v=q.top();q.pop();
			color[v]=tot;
		}while(v!=u);
	}
}

int main(){
	scanf("%d%d",&n,&m);
	int x,y;
	for(int i=1;i<=m;i++){
		scanf("%d%d",&x,&y);
		add(x,y),add(y,x);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]) tarjan(1,0);
	}
	return 0;
}

标签:Tarjan,int,tarjan,笔记,学习,dfn,low,head,节点
From: https://www.cnblogs.com/123456xwd/p/17929542.html

相关文章

  • 一些基于SpringBoot2.X的后台管理系统,可以作为高校毕设项目、个人学习之用
    该酒店管理系统适用于各类酒店管理,用于提高酒店内部工作效率。主要是针对酒店内部工作人员即管理员和酒店普通员工设计的。主要是具备客房预订、退房、房间信息管理、员工管理、入住管理等模块,提高了酒店内部业务的运转效率,大大降低了成本;该系统基于SpringBoot+VUE+MyBatisPlus......
  • 机器学习、机器视觉、机器触觉、机器听觉都是些啥?【都归属于AI吗?】
    首先,回答下标题这个疑问句?----YES 简述下对应的发展历史:1956年,第一个AI会议在达特茅斯学院举行,标志着AI作为学科的正式创立。会议的主要发起人——约翰·麦卡锡(JohnMcCarthy),计算科学家、认知科学家,也是他提出了“人工智能”的概念。如图1.20世纪60年代至70年代,符号推理(Symbolic......
  • Kotlin从入门到精通,正确的学习路径+学习资料
    前言Kotlin是一种针对Java平台的新编程语言。它简洁、安全、务实,专注于与Java的互操作性,可以很好地与所有现存的Java库和框架一起工作,且性能与Java相当。Kotlin可以用于几乎所有Java使用的地方,如服务端开发、Android应用开发等。如何学习学习Kotlin从入门到精通需要按照一定的步......
  • 机器学习-无监督机器学习-高斯混合模型-22
    目录1.2.GMM算法的一般流程3.使用模型1.假设不同的簇数据来自于不同的高斯分布。或者换句话说,高斯混合模型就是当成数据集是由多个高斯分布混合而成的。这是这个模型的核心思想.一维的gauss分布:多变量(比如d个变量)高斯分布的概率密度函数:μ是一个n维向量,对应着分布的均......
  • 一个专为量化投资开发的强化学习算法框架:ElegantRL
    链接:https://github.com/AI4Finance-Foundation/ElegantRL这是一个专为量化投资开发的强化学习算法框架。相关论文:ElegantRL-Podracer:ScalableandElasticLibraryforCloud-NativeDeepReinforcementLearning......
  • Node.js+Express+Koa2开发接口学习笔记(三)
    数据库操作(创建和增删查)使用Navicat快速创建myblog数据库创建表使用navicat快速建表使用sql语句进行简单的查询--showtables;--显示该数据库中的所有表INSERTINTOusers(username,`password`,realname)VALUES('zhangsan','123','张三')INSERTINTOusers(......
  • Web 学习记录
    写在前面高中竞赛期间晚上不想写题,给自己找点乐子。当然也是为CTF做准备。具体的学习顺序参考《Web深度剖析》。一些网站:菜鸟教程,w3school在线教程,ctf-wiki.com。WebHTML感觉这东西没啥用,遇到不会的东西在来看。SQLXSS......
  • 《程序员的修炼之道》第二章读书笔记
    第2章《注重实效的途径》是《程序员的修炼之道》中的重要章节,它介绍了一些实践性的方法和技巧,帮助程序员在软件开发中提高效率和质量。在这一章中,作者首先强调了重复的危害。重复的代码和流程可能导致维护难度和出现错误的概率增加。因此,我们需要通过技术手段和工具来减少重复,如自......
  • 持续学习概览
    持续学习两个主要问题:灾难性遗忘稳定性-可塑性权衡baselineFinetune根据任务顺序依次微调这个方法是持续学习的下界Multi-taskLearning多任务学习这个方法是持续学习的上界基于Replay(回放)的方法对之前的关键数据,或模型梯度进行存储或压缩存储。在学习新任务时,为减......
  • 《自我边界》乔治戴德 笔记
    前言我们大部分人为了追求舒适,都会刻意与他人保持相应的距离(除非是与我们很亲近的人)。与他人相距太近,我们会认为不舒服,而太远,我们又觉得不够友善。这里讲述了一个送礼的故事,送礼的人跟接受礼物的人。如果接受礼物的人表达不满情绪(愤怒、猜忌)。送礼的人就会很难受。结局送的鲜花......