首页 > 编程语言 >Tarjan 算法学习笔记

Tarjan 算法学习笔记

时间:2023-04-08 16:35:25浏览次数:44  
标签:Tarjan int 连通 笔记 算法 dfn low 节点 分量

(绝大部分都是贺的,来自 OI-WIKI 和 洛谷题解 ,自己抄一遍印象深刻一点,部分代码未编译,不保证正确性,但大体是对的)

一、DFS 生成树

注意可能有多棵,因为图可能不联通。

  1. 树边(tree edge):示意图中以黑色边表示,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
  2. 反祖边(back edge):示意图中以红色边表示(即 ),也被叫做回边,即指向祖先结点的边。
  3. 横叉边(cross edge):示意图中以蓝色边表示(即 ),它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先。
  4. 前向边(forward edge):示意图中以绿色边表示(即 ),它是在搜索的时候遇到子树中的结点的时候形成的。

二、强连通分量

1.定义:

强连通:一个有向图,其任意一个节点都可以到达另一个节点。

强连通分量:一个有向图的极大强连通子图。

2.计算方法:

从一个节点开始,进行 DFS ,如果当前边指向一个已经经过的点,如果这条边是返祖边,则一定可以形成一个强连通分量。

3.数组含义:

dfnx 代表 x 号节点的 dfn 序。

inx 代表 x 号节点目前在不在栈里。

lowx 代表 x 号节点所在的强联通分量里 dfn 最小的节点的 dfn 是多少。

stt 代表存储从搜索树上目前遍历到且不属于任何一个强连通分量的 t 个点的栈。

sccx 代表 x 号节点所属的强连通分量的编号。

4.代码实现

void tarjan(int x){
  dfn[x]=low[x]=++cnt;//计算 dfn 序,而且一开始只有 x ,所以 low = dfn 
  in[x]=1;st[++t]=x;//入栈
  for(int i=h[x];i;i=nxt[i])
    if(!dfn[to[i]]){//没有遍历过,直接 dfs ,再更新 low 
      tarjan(to[i]);
      low[x]=min(low[x],low[to[i]]);
    }else if(in[to[i]])low[x]=min(low[x],dfn[to[i]]);// to[i] 可能是 x 的祖先
  if(low[x]==dfn[x]){
     tot++;//新的强连通分量
    while(st[t+1]!=x){
      scc[st[t]]=tot;
      in[st[t--]]=0;
    }
  }
}

 


 

三、割点

1.定义:

对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点。

2.计算方法:

① 对于 DFS 树的树根,它是割点当且仅当它有两个及以上的子树。

② 对于其它任意一个点,当且仅当以它为根的子树内没有向其它子树或祖先连边。

3.数组含义:

dfnx 代表 x 号节点的 dfn 序。

lowx 代表以 x 号节点为根的子树里不经过 x 的路径能到达的 dfn 最小的节点的 dfn 是多少。

cutx 代表 x 号节点是不是割点。

4.代码实现:

void tarjan(int x,int fa,int rt){
	dfn[x]=low[x]=++cnt;
	int c=0;//统计当 x=rt 时 x 的子树个数
	for(int i=h[x];i;i=nxt[i]){
		if(!dfn[to[i]]){
			tarjan(to[i],x,rt);
			low[x]=min(low[x],low[to[i]]);
			if(low[to[i]]>=dfn[x]&&x!=rt)cut[x]=1;//条件②
			if(x==rt)c++;
		}else if(to[i]!=fa)low[x]=min(low[x],dfn[to[i]]);//更新 low
	if(c>=2&&x==rt)cut[x]=1;//条件①
}

 三、割边

1.定义:对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。

2.计算方法:

首先,容易知道割边一定是 DFS 树的树边。记录 DFS 树上指向 x 的边,它是割边当且仅当以 x 为根的子树内没有向其它子树或祖先连边。

不用考虑 x 是否是根节点。

注意因为用 链式前向星 存图,所以一次只能存一个方向的边,所以可以让 cnt 从 2 开始,如果当前边编号为 i ,则其反向边编号为 i^1 。

3.数组含义:

dfnx 代表 x 号节点的 dfn 序。

lowx 代表以 x 号节点为根的子树里不经过搜索树上指向 x 的那条边的路径能到达的 dfn 最小的节点的 dfn 是多少。

cuti 代表 i 号边是不是割边。

4.代码实现:

void tarjan(int x,int van){//记录连向 x 的边(请忽略变量名) 
	dfn[x]=low[x]=++cnt;
	for(int i=h[x];i;i=nxt[i])
		if(!dfn[to[i]]){
			tarjan(to[i],i);
			low[x]=min(low[x],low[to[i]]);
			if(dfn[x]<low[to[i]])cut[i]=cut[i^1]=1;//条件,注意是 > 不是 >= ,因为研究的是从 x 出发的边 i 
               //如果 dfn[x]=low[to[i]] ,则说明下面有点能连回 x ,那么去掉 i 后以 to[i] 为根的子树与 x 仍联通, i 不为割边 }else if(i!=(van^1))low[x]=min(low[x],dfn[to[i]]);//更新 low }

  


四、点双连通分量(代码、计算方法是贺 洛谷 UID=434929 的)

1.定义:

点双连通:在一张连通的无向图中,对于两个点 u 和 v ,如果无论删去哪个点(只能删去一个,且不能删 u 和 v 自己)都不能使它们不连通,我们就说 u 和 v 点双连通。

点双连通不具有传递性。

性质:两个不同的点双连通分量最多有一个公共点,否则它们可以合成为一个更大的点双连通分量。

2.计算:

对于一个点双,它在 DFS 搜索树中 dfn 值最小的点一定是割点或者树根。

如果该点双不包含割点或树根,它一定可以继续拓展。所以我们在点双中任取一个割点或者树根。

①如果该点是树根,它的 dfn 值是整棵树里最小的。它若有两个以上子树,那么它是一个割点;它若只有一个子树,它一定属于它的直系儿子的点双,因为包括它;它若是一个独立点,视作一个单独的点双。

②当这个点是割点时,它所属的点双必定不可以向它的父亲方向包括更多点,因为一旦回溯,它就成为了新的子图的一个割点,不是点双。所以它应该归到其中一个或多个子树里的点双中。

3.数组含义:

dfnx 代表 x 号节点的 dfn 序。

lowx 代表 x 号节点所在的点双联通分量里 dfn 最小的节点的 dfn 是多少。

stt 代表存储从搜索树上目前遍历到且还可以属于其它点双连通分量的 t 个点的栈。

bcc 代表 x 号节点所属的点双连通分量的编号(这里用 ans 记录了)。

4.代码实现:

void tarjan(int x,int fa){
	int son=0;//为孤点或根节点统计子树个数 
	dfn[st[++t]=x]=low[x]=++cnt;//进栈、初始化 
	for(int i=h[x];i;i=nxt[i])
		if(!dfn[to[i]]){
			son++;tarjan(to[i],x);
			low[x]=min(low[x],low[to[i]]);
			if(low[to[i]]>=dfn[x]){//情况②, x 是割点,则有一个从 x 开始的点双连通分量 
				bcc++;//点双个数增加 
				while(st[t+1]!=to[i])ans[bcc].push_back(st[t--]);//退栈、记录 
				ans[bcc].push_back(x);
			}
		}else if(to[i]!=fa)low[x]=min(low[x],dfn[to[i]]);//更新 low 
	if(fa==0&&son==0)ans[++bcc].push_back(x);//情况①,处理孤点或根节点统计子树个数 
}

  


五、边双连通分量(代码、计算方法是贺 洛谷 UID=425694的)

1.定义:

在一张连通的无向图中,对于两个点 u 和 v,如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说 u 和 v 边双连通。

 边双连通具有传递性,即,若 x、y 边双连通,x、z 边双连通,则 y、z 边双连通。

2.计算方法:

把图中所有割边去掉就行。因为如果一个边双有割边,去掉之后一定不连通。

3.数组含义:

dfnx 代表 x 号节点的 dfn 序。

lowx 代表以 x 号节点为根的子树里不经过搜索树上指向 x 的那条边的路径能到达的 dfn 最小的节点的 dfn 是多少。

cuti 代表 i 号边是不是割边。

dccx 记录了 x 所在的边双连通分量的编号。

4.代码实现:

void dfs(int x,int num){
	dcc[x]=num;//记录所在变双编号 
	ans[num].push_back(x);
	for(int i=h[x];i;i=nxt[i])
		if(!dcc[to[i]]&&!cut[i])dfs(to[i],num);
	//如果没被填色并且连的边不是割边就标记为同一块 
}
void tarjan(int x,int van){//求割边的板子 
	dfn[x]=low[x]=++cnt;
	for(int i=h[x];i;i=nxt[i])
		if(!dfn[to[i]]){
			tarjan(to[i],i);
			if(dfn[x]<low[to[i]])cut[i]=cut[i^1]=1;
			low[x]=min(low[x],low[to[i]]);
		}else if(i!=(van^1))low[x]=min(low[x],dfn[to[i]]);
}

  

六、动态树

听说好像也是 Robert E. Tarjan 发明的,但是本蒟蒻不会。

七、结语

本蒟蒻贺了一晚上,希望大家能看懂(当然我这个主要还是为了自己整理),记得给原作者点赞。

所有的 Tarjan 主要就是维护 dfn 和 low 数组,再用 st 记录一下值。本质都很像,学会举一反三。

标签:Tarjan,int,连通,笔记,算法,dfn,low,节点,分量
From: https://www.cnblogs.com/qwq-qaq-tat/p/17297210.html

相关文章

  • 【ZYNQ】学习笔记:VDMA彩条显示实验Part2
    【学习视频】正点原子https://www.bilibili.com/video/BV11j411f7Co===================================================================【ZYNQ】学习笔记:VDMA彩条显示实验Part1  https://www.cnblogs.com/steven913/p/17298510.html====================================......
  • Python 进阶指南(编程轻松进阶):十三、性能测量和大 O 算法分析
    原文:http://inventwithpython.com/beyond/chapter13.html对于大多数小程序来说,性能并不那么重要。我们可能会花一个小时编写一个脚本来自动执行一个只需要几秒钟就能运行的任务。即使需要更长的时间,当我们端着一杯咖啡回到办公桌时,这个项目也可能已经完成了。有时候花时间学......
  • Python Qt 图形界面编程PySide2学习笔记
    内容来源:PythonQt简介安装_哔哩哔哩_bilibili1.使用QTDesigner对UI进行布局,不需要改代码,只保存.ui文件即可2.如果已有控件,想要做到自适应界面,要选中多个控件,右键选择Layout布局方式。3.对于单个控件,可以先拖入一个Layout项(垂直或水平Layout)后,再将该控件拖到右侧Layout项上进......
  • Chapter2 K-近邻算法案例1
    案例2:使用K-近邻算法实现手写数字系统1.案例要求编写一个程序,应用K-近邻算法,实现手写数字系统。通过画图生成一个32*32的数字图像,再将图像转化为代表数字的0-1文本文件。之后往程序输入代表数字的0-1文本文件,程序便可以输出相应的数字。2.案例的执行流程示例......
  • 【生产调度】基于和声搜索算法实现并行机器调度附matlab代码
    ✅作者简介:热爱科研的Matlab仿真开发者,修心和技术同步精进,matlab项目合作可私信。......
  • JavaScript 数组笔记
    添加和删除数组项添加push()push()方法:向数组的末尾添加一个或多个元素,并返回修改后的数组长度。语法:arr.push(element1[,...[,elementN]])参数:element1,...,elementN:要添加到数组末尾的元素。示例:constfruits=['apple','banana','orange'];constnewLength......
  • 算法C#
    #region二分查找法publicstaticintBinarySertch(int[]arr,intstartIndex,intendIndex,intresult){if(startIndex>endIndex){return-1;}intmidIndex=(end......
  • Java笔记(六):设计原则
    SOLID原则是面向对象设计和编程中的一组基本原则,其中SOLID分别是以下五个原则的首字母缩写:单一职责原则(SingleResponsibilityPrinciple,SRP)。一个类或者模块只应该有一个单一的责任。这个原则告诉我们,一个类应该只负责一项功能,不要试图把太多的职责塞到一个类里面。开闭......
  • 【ZYNQ】笔记:VDMA彩条显示实验
    【学习视频】正点原子https://www.bilibili.com/video/BV11j411f7Co===================================================================【学习笔记】【1】DDR的帧缓存操作:VDMA写数据至DDR;VDMA再从DDR中读取数据。作用:解决视频源与显示设备间速率、分辨率不匹配的问题。......
  • UE5 开发笔记
    需要在游戏过程中一直存在的代码写在哪?写在继承自UGameInstanceSubsystem类的自定义编程子系统类中。参考:《InsideUE4》GamePlay架构(十一)Subsystems-知乎(zhihu.com)......