DFS序和欧拉序的降维打击

1. DFS 序和时间戳

1.1 DFS 序

定义:树的每一个节点在深度优先遍历中进、出栈的时间序列。

如下树的 dfs 序就是[1,2,8,8,5,5,2,4,3,9,9,3,6,6,4,7,7,1]

5.png

下图为生成DFS的过程。对于一棵树进行DFS序,除了进入当前节点时对此节点进行记录,同时在回溯到当前节点时对其也记录一下,所以DFS序中一个节点的信息会出现两次。

Tips: 因为在树上深度搜索时可以选择从任一节点开始,所以DFS序不是唯一的。

6.png

DFS序的特点:

  • 可以把树数据结构转换为线性数据结构,从而可以把基于线性数据的算法间接用于处理树上的问题。堪称降维打击。

  • 相同编号之间的节点编号为以此编号为根节点的子树上的所有节点编号。

    [2,8,8,5,5,2]表示编号 2为根节点的子树中所有节点为8,5

    [4,3,9,9,3,6,6,4]表示编号 4为根节点的子树中所有节点为 3,9,6

  • 如果一个节点的编号连续相同,则此节点为叶节点。

  • 树的DFS序的长度是2NN表示节点的数量)。

2.png

DFS序的代码:

#include <cstdio>
using namespace std;
const int maxn=1e5+10;
int n;
int tot,to[maxn<<1],nxt[maxn<<1],head[maxn];
int id[maxn],cnt;
void add(int x,int y)
{to[++tot]=y;nxt[tot]=head[x];head[x]=tot;
}
void dfs(int x,int f)
{id[++cnt]=x;for(int i=head[x];i;i=nxt[i]){int y=to[i];if(y==f)continue;dfs(y,x);}id[++cnt]=x;
}
int main()
{scanf("%d",&n);for(int i=1;i<n;i++){int x,y;scanf("%d%d",&x,&y);add(x,y);add(y,x);}dfs(1,0);for(int i=1;i<=cnt;i++)printf("%d ",id[i]);return 0;
}

测试数据:

9
1 2
1 4
1 7
2 8
2 5
4 3
4 6
3 9

1.2 时间戳

按照深度优先遍历的过程,按每个节点第一次被访问的顺序,依次给予这些节点1−N的标记,这个标记就是时间戳。如果一个点的起始时间和终结时间被另一个点包括,这个点肯定是另一个点的子节点(简称括号化定理)。每棵子树 x 在 DFS 序列中一定是连续的一段,结点 x 一定在这段的开头。

7.png

dfs与时间戳的关系,对应列表中索引号和值的关系。

8.png

dfs代码中添加进入节点时的顺序和离开节点时的顺序。

//……
//in 开始时间 out 结束时间
int in[maxn],out[maxn];
//……
void dfs(int x,int f) {//节点的 dfs 序id[++cnt]=x;//开始时间in[x]=cnt;for(int i=head[x]; i; i=nxt[i]) {int y=to[i];if(y==f)continue;dfs(y,x);}id[++cnt]=x;//结束时间out[x]=cnt;
}
//……

3. DFS 序的应用

3.1 割点

什么是割点?

如果去掉一个节点以及与它连接的边,该点原来所在的图被分成两部分,则称该点为割点。如下图所示,删除 2号节点,剩下的节点之间就不能两两相互到达了。例如 4号不能到5号,6号也不能到达1号等等。一个连通分量变成两个连通分量!

9.png

怎么判断图是否存在割点以及如何找出图的割点?

Tarjan 算法是图论中非常实用且常用的算法之一,能解决强连通分量、双连通分量、割点和割边(桥)、求最近公共祖先(LCA)等问题。

Tarjan算法求解割点的核心理念:

  • 在深度优先遍历算法访问到k点时,此时图会被k点分割成已经被访问过的点和没有被访问过的点。
  • 如果k点是割点,则没有被访问过的点中至少会有一个点在不经过k点的情况下,是无论如何再也回不到已访问过的点了。则可证明k点是割点。

下图是深度优先遍历访问到2号顶点的时候。没有被访问到的顶点有4、5、6号顶点。

Tips: 节点边上的数字表示时间戳。

10.png

其中56号顶点都不可能在不经过2号顶点的情况下,再次回到已被访问过的顶点(13号顶点),因此2号顶点是割点。

问题变成如何在深度搜索到 k点时判断,没有被访问过的点是否能通过此k或者不能通过此k点回到曾经访问过的点。

算法中引入了回溯值概念。

回溯值表示从当前节点能回访到时间戳最小的祖先,回溯值一般使用名为 low的数组存储,low[i]表示节点 i的回溯值。

如下演示如何初始化以及更新节点的 low值。

  • 定义3 个数组。vis[i]记录节点是否访问过、dfn[i]记录节点的时间戳、low[i]记录节点的回溯值。如下图所示,从 1号节点开始深搜,搜索到4号节点时,3个数组中的值的变化如下。也就是说,初始,节点的 low值和dfn值相同。或者说此时,回溯值还不能确定。

    Tips:注意一个细节,由1->3,认为 13的父节点。

11.png

  • 搜索到4号时,与4号相连的边有4->14->1是没有访问过的边,且1号节点已经标记过访问过,也就是说通过4号点又回到了1号点。所以说4->1是一条回边,或者说 1-……-4之间存在一个环。则4号点的 low[4]=min( low[4],dfn[1] )=1

12.png

  • 因为 24的父节点,显然也是能通过4号点回到1号点,所以也需要更新其low值,更新表达式为 low[2]=min(low[2],low[4])。同理3号点是2号点的父节点,也能通过 3->2->4->1回到1号点。所以3号点的low也需要更新。low[3]=min(low[2],low[3])

13.png

  • 继续更新5、6号节点的low值。

14.png

根据这些信息,如何判断割点。

  • 如果当前点为根节点时,若子树数量大于一,则说明该点为割点(子树数量不等于与该点连接的边数)。
  • 如果当前点不为根节点,若存在一个儿子节点的low值大于或等于该点的dfn值时(low[子节点] >= dfn[父节点]),该点为割点(即子节点,无法通过回边,到达某一部分节点(这些节点的dfn值小于父亲节点))。这个道理是很好理解的,说明子节点想重回树的根节点是无法绕开父节点。

3.2 割边

定义:即在一个无向连通图中,如果删除某条边后,图不再连通。如下图删除2-55-6后,图不再具有连通性。
15.png

删除2-55-6边后。

16.png

那么如何求割边呢?

只需要将求割点的算法修改一个符号就可以。只需将low[v]>=num[u]改为low[v]>num[u],取消一个等于号即可。因为low[v>=num[u]代表的是点v 是不可能在不经过父亲结点u而回到祖先(包括父亲)的,所以顶点u是割点。

如果low[y]和num[x]相等则表示还可以回到父亲,而low[v]>num[u]则表示连父亲都回不到了。倘若顶点v不能回到祖先,也没有另外一条路能回到父亲,那么 w-v这条边就是割边,

3.3 Tarjan 算法

#include <iostream>
#include <string.h>
#include <string>
#include <algorithm>
#include <math.h>
#include <vector>
using namespace std;
const int maxn = 123456;
int n, m, dfn[maxn], low[maxn], vis[maxn], ans, tim;bool cut[maxn];
vector<int> edge[maxn];void cut_bri(int cur, int pop) {vis[cur] = 1;// 1表示正在访问中dfn[cur] = low[cur] = ++tim;int children = 0; //子树数量for (int i : edge[cur]) { //对于每一条边if (i == pop || vis[cur] == 2)continue;if (vis[i] == 1) //遇到回边low[cur] = min(low[cur], dfn[i]); //回边处的更新 (有环)if (vis[i] == 0) {cut_bri(i, cur);children++;  //记录子树数目low[cur] = min(low[cur], low[i]); //父子节点处的回溯更新if ((pop == -1 && children > 1) || (pop != -1 && low[i] >= dfn[cur])) { //判断割点if (!cut[cur])ans++;   //记录割点个数cut[cur] = true; //处理割点}if(low[i]>dfn[cur]) { //判断割边edge[cur][i]=edge[i][cur]=true;  //low[i]>dfn[cur]即说明(i,cur)是桥(割边);}}}vis[cur] = 2; //标记已访问
}
int main() {scanf("%d%d", &n, &m);for (int i = 1; i <= m; i++) {int x, y;scanf("%d%d", &x, &y);edge[x].push_back(y);edge[y].push_back(x);}for (int i = 1; i <= n; i++) {if (vis[i] == 0)cut_bri(i, -1); //防止原来的图并不是一个连通块//对于每个连通块调用一次cut_bri}printf("%d\n", ans);for (int i = 1; i <= n; i++) //输出割点if (cut[i])printf("%d ", i);return 0;
}

4.欧拉序

定义:进入节点时记录,每次遍历完一个子节点时,返回到此节点记录,得到的 2 ∗ N − 1 长的序列;

欧拉序和DFS序的区别,前者在每一个子节点访问后都要记录自己,后者只需要访问完所有子节点后再记录一次。如下图的欧拉序就是:
1 2 8 2 5 2 1 7 1 4 3 9 3 4 6 4 1。每个点在欧拉序中出现的次数等于这个点的度数,因为DFS到的时候加进一次,回去的时候也加进。

17.png

1.png

性质:

  • 节点 x 第一次出现与最后一次出现的位置之间的节点均为 x 的子节点;

  • 任意两个节点的 LCA 是欧拉序中两节点第一次出现位置中深度最小的节点。两个节点第一次出现的位置之间一定有它们的LCA,并且,这个LCA一定是这个区间中深度最小的点。

根据欧拉序的性质,可以用来求解CLA。如上图,求解 LCA(9,6)

  • 在欧拉序中找到96第一次出现的位置。

18.png

  • 直观比较,知道4号节点是其LCA,特征是96之间深度最小的节点。

欧拉序求LCA,先求图的欧拉序、时间戳(可以记录进入和离开节点的时间)以及节点深度。有了这些信息,理论上足以求出任意两点的LCA。变成了典型的RMQ问题。

19.png

为了提升多次查询性能,可以使用ST表根据节点的深度缓存节点的信息。j=0时如下图所示。

20.png

j=1表示区间长度为 2,值为区间长度为 1的两个子区间的深度值小的节点。

21.png

欧拉序求LCA

#include <iostream>
#include <string.h>
#include <string>
#include <algorithm>
#include <math.h>
#include <vector>
using namespace std;
const int maxn = 10000;
int n, m, dfn[maxn], dep[maxn], tim;
int ol[maxn];
int st[maxn][maxn],lg2[maxn];
vector<int> edge[maxn];
void dfs(int cur, int fa) {ol[++tim]=cur;dfn[cur]=tim;dep[cur]=dep[fa]+1;for (int v : edge[cur]) { //对于每一条边if(v==fa)continue;dfs(v,cur);ol[++tim]=cur;}
}void stPreprocess() {lg2[0] = -1;  // 预处理 lg 代替库函数 log2 来优化常数for (int i = 1; i <= (n << 1); ++i) {lg2[i] = lg2[i >> 1] + 1;}for (int i = 1; i <= (n << 1) - 1; ++i) {st[i][0] = ol[i];}for (int j = 1; j <= lg2[(n << 1) - 1]; ++j) {for (int i = 1; i + (1 << j) - 1 <= ((n << 1) - 1); ++i) {st[i][j] = dep[ st[i] [ j - 1 ] ] < dep[ st[ i + (1 << j - 1)][j - 1 ]    ]  ? st[i][j - 1 ] : st[ i + (1 << j - 1)][j - 1 ];}cout<<endl;}
}int getlca(int u, int v) {if(dfn[u]>dfn[v])swap(u,v);u=dfn[u],v=dfn[v];int d=lg2[v-u+1];int f1=st[ u ][d  ];int f2=st[v-(1<<d)+1 ][ d ];return dep[f1]<dep[f2]?f1:f2;
}int main() {scanf("%d%d", &n, &m);for (int i = 1; i <= m; i++) {int x, y;scanf("%d%d", &x, &y);edge[x].push_back(y);edge[y].push_back(x);}dfs(1, 0);for (int i = 1; i <= 2*n-1; i++) //输出割点printf("%d-%d  ", ol[i],dfn[ ol[i] ]);stPreprocess();int u,v;cin>>u>>v;int res=getlca(u,v);cout<< res;return 0;
}

5. 总结

DFS序和欧拉序并不难理解,正如四两拨千斤,却能解决很多复杂的问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/166546.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多线程Thread(初阶二:Thread类及常⻅⽅法)

目录 一、Thread 的常⻅构造⽅法 继承Thread代码&#xff1a; 实现Runnable接口代码: 二、Thread 的⼏个常⻅属性 1、id&#xff1a; 2、获取线程的名字。 3、进程的状态&#xff1a; 4、在java中设置的优先级&#xff0c; 5、是否后台线程&#xff0c; 6、是否存活&a…

ubuntu22.04 arrch64版在线安装node

脚本 #安装node#下载node、npm国内镜像&#xff08;推荐&#xff09;# 判断是否安装了nodeif type -p node; thenecho "node has been installed."elsemkdir -p /home/zenglg cd /home/zenglgwget https://registry.npmmirror.com/-/binary/node/v10.14.1/node-v10.…

Linux系统编程 day04 文件和目录操作

Linux系统编程 day04 文件和目录操作 1. 文件IO1.1 open 函数1.2 close函数1.3 read函数1.4 write函数1.5 lseek函数1.6 errno变量1.7 文件示例1 读写文件1.8 文件示例2 文件大小的计算1.9 文件示例3 扩展文件大小1.10 文件示例4 perror函数的使用1.11 阻塞与非阻塞的测试 2. 文…

关于「光学神经网络」的一切:理论、应用与发展

/目录/ 一、线性运算的光学实现 1.1. 光学矩阵乘法器 1.2. 光的衍射实现线性运行 1.3. 基于Rayleigh-Sommerfeld方程的实现方法 1.4. 基于傅立叶变换的实现 1.5. 通过光干涉实现线性操作 1.6. 光的散射实现线性运行 1.7. 波分复用&#xff08;WDM&#xff09;实现线性运…

脉冲幅度调制信号的功率谱计算

本篇文章是博主在通信等领域学习时&#xff0c;用于个人学习、研究或者欣赏使用&#xff0c;并基于博主对人工智能等领域的一些理解而记录的学习摘录和笔记&#xff0c;若有不当和侵权之处&#xff0c;指出后将会立即改正&#xff0c;还望谅解。文章分类在通信领域笔记&#xf…

风口下的危与机:如何抓住生成式AI黄金发展期?

回顾AI的发展历程&#xff0c;我们见证过几次重大突破&#xff0c;比如2012年ImageNet大赛的图像识别&#xff0c;2016年AlphaGo与李世石的围棋对决&#xff0c;这些进展都为AI的普及应用铺设了道路。而ChatGPT的出现&#xff0c;真正让AI作为一个通用的产品&#xff0c;走入大…

Linux | 创建 | 删除 | 查看 | 基本命名详解

Linux | 创建 | 删除 | 查看 | 基本命名详解 文章目录 Linux | 创建 | 删除 | 查看 | 基本命名详解前言一、安装Linux1.1 方法一&#xff1a;云服务器方式1.2 方法二&#xff1a;虚拟机方式 二、ls2.2 ll 三、which3.1 ls -ld 四、pwd五、cd5.1 cd .\.5.2 ls -al5.3 重新认识命…

程序员兼职需要收藏的防坑技巧

不管你是刚刚上车的新职员&#xff0c;还是职场经营多年的老手&#xff0c;在零散时间&#xff0c;通过兼职搞一点零花钱&#xff0c;充实一下自己的生活&#xff0c;这是在正常不过的事情&#xff0c;但是很多同学害怕兼职有风险&#xff0c;被骗或者说找不到门路&#xff0c;…

优思学院|质量工程师在汽车行业待遇好吗?

优思学院认为质量工程师在汽车行业的待遇有可能相对较好的。随着中国汽车品牌在国内市场的崛起&#xff0c;特别是在电动汽车领域的增长&#xff0c;质量工程师在保障产品质量和安全性方面变得非常重要。由于中国汽车制造商对产品质量的高度重视&#xff0c;质量工程师在制定和…

AC自动机(简单模板)

AC自动机&#xff0c;就相当于是在字典树上用kmp。next数组回退的位置为最大匹配字符串在字典树上的节点位置。 在获取字典树上的next数组的时候用的是BFS每次相当与处理的一层。 下图中红线为&#xff0c;可以回退的位置&#xff0c;没有红线的节点回退的位置都是虚拟原点。…

基于C#实现线段树

一、线段树 线段树又称"区间树”&#xff0c;在每个节点上保存一个区间&#xff0c;当然区间的划分采用折半的思想&#xff0c;叶子节点只保存一个值&#xff0c;也叫单元节点&#xff0c;所以最终的构造就是一个平衡的二叉树&#xff0c;拥有 CURD 的 O(lgN)的时间。 从…

关于同一接口有多个不同实现的设计方案

关于同一接口有多个不同实现的设计方案 前言 最近公司做了一个银行相关的项目&#xff0c;告诉我公司对接了多个银行的支付&#xff0c;每个银行都有对应的接口要去对接&#xff0c;比如&#xff1a;交易申请&#xff0c;交易取消&#xff0c;支付&#xff0c;回单&#xff0…

rabbitMQ发布确认-交换机不存在或者无法抵达队列的缓存处理

rabbitMQ在发送消息时&#xff0c;会出现交换机不存在&#xff08;交换机名字写错等消息&#xff09;&#xff0c;这种情况如何会退给生产者重新处理&#xff1f;【交换机层】 生产者发送消息时&#xff0c;消息未送达到指定的队列&#xff0c;如何消息回退&#xff1f; 核心&…

麒麟KYSEC使用方法05-命令设置密码强度

原文链接&#xff1a;麒麟KYSEC使用方法05-命令设置密码强度 hello&#xff0c;大家好啊&#xff0c;今天给大家带来麒麟KYLINOS的kysec使用方法系列文章第五篇内容----使用命令设置密码强度&#xff0c;密码强度策略有两个文件需要修改&#xff0c;pwquality.conf/login.defs&…

命令执行总结

之前做了一大堆的题目 都没有进行总结 现在来总结一下命令执行 我遇到的内容 这里我打算按照过滤进行总结 依据我做过的题目 过滤system 下面是一些常见的命令执行内容 system() passthru() exec() shell_exec() popen() proc_open() pcntl_exec() 反引号 同shell_exec() …

大语言模型概述(三):基于亚马逊云科技的研究分析与实践

上期介绍了基于亚马逊云科技的大语言模型相关研究方向&#xff0c;以及大语言模型的训练和构建优化。本期将介绍大语言模型训练在亚马逊云科技上的最佳实践。 大语言模型训练在亚马逊云科技上的最佳实践 本章节内容&#xff0c;将重点关注大语言模型在亚马逊云科技上的最佳训…

解决Chrome浏览器无法启动,因为应用程序的并行配置不正确

目录 现象 方法1 方法2 附带&#xff1a;书签路径 一次比较奇怪的问题&#xff0c;花了一些时间&#xff0c;记录下来。 现象 进到本机默认安装路径&#xff1a; C:\Users\你的用户名\AppData\Local\Google\Chrome\Application 下面会有个版本号的目录&#xff0c;如我的…

快手ConnectionError

因为运行的程序被中断导致 top然后查看站用处内存高的accelerate kill进程号 9回车

linux基础5:linux进程1(冯诺依曼体系结构+os管理+进程状态1)

冯诺依曼体系结构os管理 一.冯诺依曼体系结构&#xff1a;1.简单介绍&#xff08;准备一&#xff09;2.场景&#xff1a;1.程序的运行&#xff1a;2.登录qq发送消息&#xff1a; 3.为什么需要内存&#xff1a;1.简单的引入&#xff1a;2.计算机存储体系&#xff1a;3.内存的意义…

微服务知识小结

1. SOA、分布式、微服务之间有什么关系和区别&#xff1f; 1.分布式架构指将单体架构中的各个部分拆分&#xff0c;然后部署到不同的机器或进程中去&#xff0c;SOA和微服务基本上都是分布式架构的 2. SOA是一种面向服务的架构&#xff0c;系统的所有服务都注册在总线上&#…