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序和欧拉序并不难理解,正如四两拨千斤,却能解决很多复杂的问题。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PSP - 从头搭建 抗原类别 (GPCR) 的 蛋白质结构预测 项目流程

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/134595717 GPCRs&#xff08;G Protein-Coupled Receptors&#xff0c;G蛋白偶联受体&#xff09;&#xff0c;又称为7次跨膜受体&#xff0c;是细…

浅析jdk8所包含的主要特性

至今Java 8仍然是许多开发者首选的JDK版本&#xff0c;Java 8的生态系统非常成熟&#xff0c;许多库和框架都已经适配了Java 8。迁移到新的Java版本可能需要重新评估和调整现有的依赖关系&#xff0c;这对于一些大型项目可能是一个挑战。那么Java 8有哪些特性让多数开发者钟爱呢…

生产实践:Redis与Mysql的数据强一致性方案

公众号「架构成长指南」&#xff0c;专注于生产实践、云原生、分布式系统、大数据技术分享。 数据库和Redis如何保存强一致性&#xff0c;这篇文章告诉你 目的 Redis和Msql来保持数据同步&#xff0c;并且强一致&#xff0c;以此来提高对应接口的响应速度&#xff0c;刚开始考…

四数之和java版

题目描述 给定一个包含 n 个整数的数组 nums 和一个目标值 target&#xff0c;判断 nums 中是否存在四个元素 a&#xff0c;b&#xff0c;c 和 d &#xff0c;使得 a b c d 的值与 target 相等&#xff1f;找出所有满足条件且不重复的四元组。 注意&#xff1a;答案中不可以…

conan 入门(三十二):package_info中配置禁用CMakeDeps生成使用项目自己生成的config.cmake

conanfile.py中定义的package_info()方法用于向package的调用者(conumer)提供包库名&#xff0c;编译/连接选项&#xff0c;文件夹等等信息&#xff0c;有了这些信息构建工具的generator就可以根据它们生成对应的文件&#xff0c;用于调用者引用package. 比如基于cmake的CMakeD…

系列四、编程式事务

一、概述 编程式事务是指程序员手动的在业务代码中控制事务执行的流程&#xff0c;业务方法正常执行提交事务&#xff0c;业务方法执行过程中出现异常则回滚事务。 二、编程式事务环境搭建 2.1、项目概览 2.2、pom.xml <dependencies><!--spring基本依赖--><d…

20s上手!文本生成3D模型

公众号&#xff1a;算法一只狗 硅谷初创公司Luma AI发布了一款名为Genie的Discord机器人&#xff0c;用于生成文本到3D内容&#xff0c;为游戏开发、虚拟制作和艺术创作带来变革。用户只需输入文本指令&#xff0c;Genie即可在20秒内生成四个简单的3D模型&#xff0c;并支持进一…

Ubuntu20安装ssh服务

Ubuntu20上执行如下命令查看是否存在ssh服务 #ps -e | grep ssh 只有ssh-agent&#xff0c;没有sshd; 因此要安装openssh-server. 搜索openssh-server,得到下载链接&#xff1a; openssh-server 复制这个Binary Package链接即可下载&#xff0c;然后使用如下命令安装 sudo…

【C++】list的介绍与使用

&#x1f9d1;‍&#x1f393;个人主页&#xff1a;简 料 &#x1f3c6;所属专栏&#xff1a;C &#x1f3c6;个人社区&#xff1a;越努力越幸运社区 &#x1f3c6;简 介&#xff1a;简料简料&#xff0c;简单有料~在校大学生一枚&#xff0c;专注C/C/GO的干货分…

基于opencv+ImageAI+tensorflow的智能动漫人物识别系统——深度学习算法应用(含python、JS、模型源码)+数据集(一)

目录 前言总体设计系统整体结构图系统流程图 运行环境爬虫1.安装Anaconda2.安装Python3.63.更换pip源4.安装Python包5.下载phantomjs 模型训练1.安装依赖2.安装lmageAl 实际应用1.前端2.安装Flask3.安装Nginx 相关其它博客工程源代码下载其它资料下载 前言 本项目通过爬虫技术…

JVM-基础

jdk7及以前&#xff1a; 通过-XX:PermSize 来设置永久代初始分配空间&#xff0c;默认值是20.75m -XX:MaxPermSize来设定永久代最大可分配空间&#xff0c;32位是64m&#xff0c;64位是82m jdk8及之后&#xff1a; 通过-XX:MetaspaceSize 来设置永久代初始分配空间&#xff…

Linux python安装 虚拟环境 virtualenv

根目录创建 venvs 文件夹 sudo mkdir /venvs 进入 /venvs 目录 cd /venvsp 创建虚拟环境&#xff0c;前提要按照 python3 安装 的 命令 sudo apt install python3 sudo python3 -m venv 虚拟环境名 激活虚拟环境 source /venvs/zen-venv/bin/activate 安装flask pip install fl…

小程序中的大道理之二--抽象与封装

继续扒 接着 上一篇 的叙述, 健壮性也有了, 现在是时候处理点实际的东西了, 但我们依然不会一步到底, 让我们来看看. 一而再地抽象(Abstraction Again) 让我们继续无视那些空格以及星号等细节, 我们看到什么呢? 我们只看到一整行的内容, 当传入 3 时就有 3 行, 传入 4 时就…

2023-11-24 事业-代号s-行业数据研报网站-记录

摘要&#xff1a; 2023-11-24 事业-代号s-行业数据研报网站-记录 行业数据研报网站 1、萝卜投研&#xff1a;https://robo.datayes.com 看数据、下载研报、上市公司PE/PB研究等。2、镝数聚&#xff1a;www.dydata.io 全行业数据&报告查找下载平台&#xff0c;覆盖100行业报…

关于python 语音转字幕,字幕转语音大杂烩

文字转语音 Python语音合成之第三方库gTTs/pyttsx3/speech横评(内附使用方法)_python_脚本之家 代码示例 from gtts import gTTStts gTTS(你好你在哪儿&#xff01;,langzh-CN)tts.save(hello.mp3)import pyttsx3engine pyttsx3.init() #创建对象"""语速"…

Unity使用DOTween实现分段进度条

文章目录 需求下载安装 DOTween实现实现效果 需求 用组件进度条&#xff08;Slider&#xff09;&#xff0c;利用分段加载进行以假乱真的进度效果&#xff0c;比如说2秒钟到达20%的进度&#xff0c;10秒钟加载20%到50%进度&#xff0c;1分钟加载50%到90%的进度&#xff0c;30秒…

JMeter测试报错422 Unprocessable Entity

添加HTTP信息头&#xff1a; ​ HTTP请求-》添加-〉配置元件-》HTTP信息头管理器 ​ 如果需要送json&#xff0c;需要添加Content-Type:application/json&#xff0c;否则会报【422 Unprocessable Entity】

基于单片机的光伏发电并网系统设计(论文+源码)

1.系统设计 片作为主控制器。由于太阳能板本身的能量输出受到负载影响&#xff0c;因此需要在太阳能板后面加入一级DC/DC电路&#xff0c;来实现最大功率跟踪&#xff0c;以提高整个系统的效率。接着&#xff0c;由于光伏逆变器需要产生220V的交流电给居民使用&#xff0c;因此…

win10 eclipse安装教程 (java)

前言&#xff1a;安装eclipse之前必须安装JDK&#xff0c;JDK是编译环境&#xff0c;eclipse是集成开发平台。 一、JDK的安装 Java Development Kit 简称 JDK (一) 官方下载地址&#xff1a; Java Archive Downloads - Java SE 8u211 and later (oracle.com) 找到&#xff…

麒麟KYSEC使用方法04-开启及关闭fpro

原文链接&#xff1a;麒麟KYSEC使用方法04-开启及关闭fpro hello&#xff0c;大家好啊&#xff0c;今天给大家带来麒麟KYLINOS的kysec使用方法系列文章第四篇内容----使用命令开启及关闭fpro&#xff0c;文件保护策略有两种模式&#xff0c;off/on&#xff0c;今天给大家介绍一…