0x21 树与图的遍历

0x21 树与图的遍历

树与图最常见的储存方式就是使用一个邻接表保存它们的边集。邻接表以head数组为表头,使用veredge数组分别存储边的终点和权值,使用next数组模拟链表指针(就像我们在0x13节中讲解邻接表所给出的代码那样)。

1.树与图的深度优先遍历,树的DFS序、深度和重心

深度优先遍历,就是在每个点 x x x上面对多条分支时,任意选一条边走下去,执行递归,直至回溯到点 x x x后,再考虑走向其他的边,如下图所示。根据上面提到的存储方式,我们可以采用下面的代码,调用 d f s ( 1 ) dfs(1) dfs(1),对一张图进行深度优先遍历。

在这里插入图片描述

void dfs(int x)
{v[x]=1; //记录点x被访问过,v是visit的缩写for(int i=head[x];i;i=next[i]){int y=ver[i];if(v[y]) continue; //点y已经被访问过了dfs(y);}
}

这段代码访问每个点和每条边恰好一次(如果是无向边,正反个各访问一次),其时间复杂度为 O ( N + M ) O(N+M) O(N+M),其中 M M M为边数。以这段代码为框架,我们可以统计许多关于树和图的基本信息。

时间戳

按照上述深度优先遍历的过程,以每个节点第一次被访问( v [ x ] v[x] v[x]被赋值为1时)的顺序,依次给予这 N N N个节点 1 ∼ N 1\sim N 1N的整数标记,该标记就被称为时间戳,记为 d f n dfn dfn

例如,在上图中, d f n [ 1 ] = 1 , d f n [ 2 ] = 2 , d f n [ 8 ] = 3 , d f n [ 5 ] = 4 , d f n [ 7 ] = 5 , d f n [ 4 ] = 6 , d f n [ 3 ] = 7 , d f n [ 9 ] = 8 , d f n [ 6 ] = 9 dfn[1]=1,dfn[2]=2,dfn[8]=3,dfn[5]=4,dfn[7]=5,dfn[4]=6,dfn[3]=7,dfn[9]=8,dfn[6]=9 dfn[1]=1,dfn[2]=2,dfn[8]=3,dfn[5]=4,dfn[7]=5,dfn[4]=6,dfn[3]=7,dfn[9]=8,dfn[6]=9

树的DFS​序

一般来说,我们在对树进行深度优先遍历时,对于每个节点,在刚入递归后以及即将回溯前各记录一次该点的编号,最后产生的长度为 2 N 2N 2N的节点序列就称为树的 D F S DFS DFS序。

树的DFS可以不使用 v v v数组去记录每个点是否被访问过,而在DFS中加入这个节点的父节点,只要不遍历回父节点,就会一直向子节点遍历(利用了树每一个节点只有一个父节点)。

void dfs(int x)
{a[++m]=x; //a数组存储DFS序v[x]=1; //记录点x被访问过for(int i=head[x];i;i=next[i]){int y=ver[i];if(v[y]) continue;dfs(y);}a[++m]=x;
}

D F S DFS DFS序的特点是:每个节点 x x x的编号在序列中恰好出现两次。设这两次出现的位置为 L [ x ] L[x] L[x] R [ x ] R[x] R[x],那么闭区间 [ L [ x ] , R [ x ] ] [L[x],R[x]] [L[x],R[x]]就是以 x x x为根的子树的 D F S DFS DFS序。这使我们在很多树相关的问题中,可以通过 D F S DFS DFS序把子树统计转化为序列上的区间统计。

在这里插入图片描述

另外,二叉树的先序、中序与后序遍历序列,也就是通过深度优先遍历产生的,大多数程序设计入门级的书籍上都有详细讲解,在此就不再赘述。读者应该掌握这几种遍历,以及它们之间的联系与转化。

树的深度(自顶而下统计)

树中各个节点的深度是一种自顶而下的统计信息。起初,我们已知根节点的深度为0。若节点 x x x的深度为 d [ x ] d[x] d[x],则它的子节点 y y y的深度就是 d [ y ] = d [ x ] + 1 d[y]=d[x]+1 d[y]=d[x]+1。在深度优先遍历的过程中结合自顶而下的递推,就可以求出每个节点的深度 d d d

void dfs(int x)
{v[x]=1;for(int i=head[x];i;i=next[i]){int y=ver[i];if(v[y]) continue;d[y]=d[x]+1;dfs(y);}
}

树的重心(自底而上统计)

当然,也有很多信息是自底而上进行统计的,比如以每个节点 x x x为根的子树大小 s i z e [ x ] size[x] size[x]。对于叶子节点,我们已知“以它为根的子树”大小为1。若节点 x x x k k k个子节点 y 1 ∼ y k y_1\sim y_k y1yk,并且以 y 1 ∼ y k y_1\sim y_k y1yk为根的子树大小分别是 s i z e [ y 1 ] , s i z e [ y 2 ] , . . . , s i z e [ y k ] size[y_1],size[y_2],...,size[y_k] size[y1],size[y2],...,size[yk],则以 x x x为根的子树的大小就是 s i z e [ x ] = s i z e [ y 1 ] + s i z e [ y 2 ] + . . . + s i z e [ y k ] + 1 size[x]=size[y_1]+size[y_2]+...+size[y_k]+1 size[x]=size[y1]+size[y2]+...+size[yk]+1

在这里插入图片描述

对于一个节点 x x x,如果我们把它从树中删除,那么原来的一棵树可能就会分成若干个不相连的的部分,其中每一部分都是一棵子树。设 m a x _ p a r t ( x ) max\_part(x) max_part(x)表示在删除节点 x x x后产生的子树中,最大的一棵的大小。使 m a x _ p a r t max\_part max_part函数取到最小值的节点 p p p就被称为整棵树的重心。例如上图数的重心应该是节点1。通过下面的代码,我们可以统计出 s i z e size size数组,并求出树的重心。

void dfs(int x)
{v[x]=1;size[x]=1; //子树的大小int max_part=0;   //删掉x后分成的最大子树的大小for(int i=head[x];i;i=next[i]){int y=ver[i];if(v[y]) continue;dfs(y);size[x]+=size[y]; //从子节点向父节点递推max_part=max(max_part,size[y]);}max_part=max(max_part,n-size[x]); //n为整棵树的节点数目if(max_part<ans){ans=max_part; //全局变量ans记录重心对应的max_part值pos=x;        //全局变量pos记录重心}    
}

图的连通块划分

上面的代码每从 x x x开始一次遍历,就会访问 x x x能够到达的所有的点与边。因此,通过多次深度优先遍历,可以划分出一张无向图中的各个连通块。同理,对于一个森林进行深度优先遍历,可以划分出森林中的每棵树。如下面的代码所示, c n t cnt cnt就是无向图包含的连通块的个数, v v v数组标记了每个点属于哪个连通块。

void dfs(int x)
{v[x]=cnt;for(int i=head[x];i;i=next[i]){int y=ver[i];if(v[y]) continue;dfs(y);}
}
for(int i=1;i<=n;++i) //在int main()中
{if(!v[i]){cnt++;dfs(i);}
}

2.树与图的广度优先遍历,拓扑排序

树与图的广度优先遍历需要使用一个队列来实现。起初,队列中仅包含一个起点(例如1号节点)。在广度优先遍历的过程中,我们不断从队头取出一个节点 x x x,对于 x x x面对的多条分支,把沿着每条分支到达的下一个节点(如果尚未访问过)插入队尾。重复执行上述过程直到队列为空。

在这里插入图片描述

我们可以采用下面的代码对一张图进行广度优先遍历(关于代码中的 S T L q u e u e STL\ queue STL queue,参见0x71节)。

void bfs()
{memset(d,0,sizeof(d));queue<int> q;q.push(1);d[1]=1;while(!q.empty()){int x=q.front();q.pop();for(int i=head[x];i;i=next[i]){int y=ver[i];if(d[y]) continue;d[y]=d[x]+1;q.push(y);}}
}

在上面的代码中,我们在广度优先遍历的过程中顺便求出了一个 d d d数组。对于一棵树来讲, d [ x ] d[x] d[x]就是点 x x x在树中的深度。对于一张图来讲, d [ x ] d[x] d[x]被称为点 x x x的层次(从起点1走到点 x x x需要经过的最少点数)。从代码和示意图中我们可以发现,广度优先遍历是一种按照层次顺序进行访问的方法,它具有如下两个重要性质:

1.在访问完所有的第 i i i层节点后,才会开始访问第 i + 1 i+1 i+1层节点

2.任意时刻,队列中至多有两个层次的节点。若其中一部分节点属于第 i i i层,则另一部分节点属于 i + 1 i+1 i+1层,并且所有第 i i i层节点排在第 i + 1 i+1 i+1层节点之前。也就是说,广度优先遍历队列中的元素关于层次满足“两段性”和“单调性”。

这两条性质是所有广度优先思想的基础。我们在0x26节的“广搜变形”中会再次提及并探讨。与深度优先遍历一样,上面这段代码的时间复杂度也是 O ( N + M ) O(N+M) O(N+M)

拓扑排序

给定一张有向无环图,若一个由图中所有点构成的序列 A A A满足:对于图中的每条边 ( x , y ) (x,y) (x,y) x x x A A A中都出现在 y y y之前,则称 A A A是该有向无环图定点的一个拓扑序。求解序列 A A A的过程就称为拓扑排序。

拓扑排序过程的思想非常简单,我们只需要不断选择图中入度为0的节点 x x x,然后把 x x x连向的点的入度减1。我们可以结合广度优先遍历的框架来高效地实现这个过程:

1.建立空的拓扑序列 A A A

2.预处理出所有点的入度 d e g [ i ] deg[i] deg[i],起初把所有入度为0的点入队。

3.取出队头节点 x x x,把 x x x加入拓扑排序的 A A A的末尾。

4.对于从 x x x出发的每条边 ( x , y ) (x,y) (x,y),把 d e g [ y ] deg[y] deg[y]减1。若被减为0,则把 y y y入队。

5.重复第 3 ∼ 4 3\sim4 34步知道队列为空,此时 A A A即为所求。

拓扑排序可以判定有向图中是否存在环。我们可以对任意有向图执行上述过程,在完成后检查 A A A序列的长度。 A A A序列的长度小于图中点的数量,则说明某些节点未被遍历,进而说明图中有环。读者可以参考下面的程序,画图模拟拓扑排序算法。

void add(int x,int y)
{ver[++tot]=y,next[tot]=head[x],head[x]=tot;deg[y]++;
}
void topsort()
{queue<int> q;for(int i=1;i<=n;++i)if(deg[i]==0) q.push(i);while(!q.empty()){int x=q.front();q.pop();a[++cnt]=x;for(int i=head[x];i;i=next[i]){int y=ver[i];if(--deg[y]==0) q.push(y);}}
}
int main()
{cin>>n>>m;for(int i=1;i<=m;++i){int x,y;scanf("%d%d",&x,&y);add(x,y);}topsort();for(int i=1;i<=cnt;++i)printf("%d ",a[i]);cout<<endl;
}

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

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

相关文章

大数据生态圈kafka在物联网中的应用测试

背景 由物联网项目中使用到了Tbox应用管理车辆&#xff0c;在上报数据的过程中&#xff0c;需要将终端产生的数据通过kafka的produce topic customer对数据进行处理后&#xff0c;放置到mysql中。完成数据二进制到json转换工作。 Kafka的使用 查看kafka的topic ./kafka-topi…

【Trino权威指南(第二版)】Trino的架构、trino架构组件、 trino连接器架构的细节、trino的查询执行模型

文章目录 一. Trino架构1. 架构概览2. 协调器3. 发现服务4. 工作节点 二. 基于连接器的架构三. 查询执行模型1. 解析—>查询计划2. 查询计划 —> 分布式查询计划3. 运行阶段3.1. 基础概念切片&#xff1a;并行单元page 与 exchange算子pipeline切片的driverOperator 3.2.…

Redis 的常见使用场景

01 缓存 作为 Key-Value 形态的内存数据库&#xff0c;Redis 最先会被想到的应用场景便是作为数据缓存。而使用 Redis 缓存数据非常简单&#xff0c;只需要通过 string 类型将序列化后的对象存起来即可&#xff0c;不过也有一些需要注意的地方&#xff1a; 必须保证不同对象的…

STL中sort的底层实现

文章目录 1、源码分析2、算法优化3、总结 在讲解STL中sort的底层原理之前&#xff0c;先引申出这样几个问题&#xff1f; ①STL中sort的底层是采用哪种或者哪几种排序&#xff1f; ②STL中sort会导致栈溢出吗&#xff1f; ③快速排序的时间复杂度是不稳定的 l o g 2 n log_2n l…

2024年顶级的9个 Android 数据恢复工具(免费和付费)

不同的事情可能会损坏您的Android手机并导致您丢失数据。但大多数时候&#xff0c;您可以使用取证工具恢复部分或全部文件。 问题可能来自手机的物理损坏、磁盘的逻辑故障、完整的系统擦除&#xff0c;或者只是简单的粗心大意。 但是&#xff0c;无论数据丢失的原因是什么&am…

docker小白第四天

docker小白第一天 什么是镜像 1、是一种轻量级、可执行的独立软件包&#xff0c;它包含运行某个软件所需的所有内容&#xff0c;我们把应用程序和配置依赖打包好形成一个可交付的运行环境(包括代码、运行时需要的库、环境变量和配置文件等)&#xff0c;这个打包好的运行环境就…

三、Spring IoC 容器和核心概念

本章概要 组件和组件管理概念 什么是组件&#xff1f;我们的期待Spring充当组件管理角色&#xff08;IoC&#xff09;组件交给Spring管理优势 Spring IoC 容器和容器实现 普通和复杂容器SpringIoC 容器介绍SpringIoC 容器具体接口和实现类SpringIoC 容器管理配置方式 Spring I…

Golang学习之路一开山篇

Golang学习之路一开山篇 初识 Golang 我第一次接触 Golang 是在2016年, 当时在深圳工作, 项目需要用Golang, 当时在犹豫要不要学还是走, 毕竟Java开发搞了很多年了, 说放弃还是有难度的, 其实也不是放弃Java, 说不定其他项目还是要使用Java. 在领导的再三劝说下, 开启了Golan…

嵌入式开发人员需要具备哪些能力?

大家好&#xff0c;今天给大家介绍嵌入式开发人员需要具备哪些能力&#xff0c;文章末尾附有分享大家一个资料包&#xff0c;差不多150多G。里面学习内容、面经、项目都比较新也比较全&#xff01;可进群免费领取。 嵌入式开发人员需要具备以下能力&#xff1a; 熟练掌握C/C语…

Kubernetes 的用法和解析 -- 2

一.集群常用指令 1.1 基础控制指令 # 查看对应资源: 状态 $ kubectl get <SOURCE_NAME> -n <NAMESPACE> -o wide [rootkube-master ~]# kubectl get pods -n kuboard -o wide# 查看对应资源: 事件信息 $ kubectl describe <SOURCE_NAME> <SOURCE_NAME_R…

产品入门第五讲:Axure交互和情境

目录 一.Axure交互和情境的介绍 1.交互介绍 概念 常见的Axure交互设计技巧 2.情境介绍 概念 常见的Axure情境设计技巧&#xff1a; 二.实例展示 1.ERP登录页到主页的跳转 2.ERP的菜单跳转到各个页面 &#x1f4da;&#x1f4da; &#x1f3c5;我是默&#xff0c;一个…

七. 使用ts写一个贪吃蛇小游戏

之前学习了几篇的ts基础&#xff0c;今天我们就使用ts来完成一个贪吃蛇的小游戏。 游戏拆解 我们将我们的任务进行简单拆解分析。 首先我们应该有一个窗口&#xff0c;我们叫做屏幕。让蛇在里面移动&#xff0c;所有我们应该想到要设计一个大盒子当作地图。考虑到食物以及蛇…

【LeetCode刷题笔记(7-1)】【Python】【四数之和】【哈希表】【中等】

文章目录 四数之和题目描述示例 1示例 2提示解决方案1&#xff1a;【四层遍历查找】解决方案2&#xff1a;【哈希表】【三层遍历】 结束语 四数之和 四数之和 题目描述 给你一个由 n 个整数组成的数组 nums &#xff0c;和一个目标值 target 。请你找出并返回满足下述全部条件…

服务器一直掉线怎么回事?

随着网络的高速发展&#xff0c;不管是网站还是游戏&#xff0c;如果遇到服务器卡顿的情况&#xff0c;会造成用户访问网站或进游戏&#xff0c;网站页面长时间无法打开&#xff0c;游戏页面运行卡顿&#xff0c;这样就很容易会造成用户的流失&#xff0c;从而导致业务亏损极大…

可视化数据监控大屏网页界面,数据大屏模版PS资料(免费UI源文件)

数据大屏模板在大数据领域被广泛应用&#xff0c;其优势在于能够将复杂的数据通过图形、图表等方式呈现出来&#xff0c;使数据更易于理解。数据大屏模板可以用来进行数据分析。通过对数据的比较、趋势分析、异常检测等&#xff0c;可以发现数据中的规律和问题&#xff0c;为决…

Appium知多少

Appium我想大家都不陌生&#xff0c;这是主流的移动自动化工具&#xff0c;但你对它真的了解么&#xff1f;为什么很多同学搭建环境时碰到各种问题也而不知该如何解决。 appium为什么英语词典查不到中文含义&#xff1f; appium是一个合成词&#xff0c;分别取自“application…

51单片机项目(21)——基于51单片机的音乐流水灯

1.功能描述 本次所做设计&#xff0c;有流水灯的功能&#xff0c;使用了16颗LED灯&#xff0c;同时还可以播放音乐。单片机存储了三首音乐&#xff0c;通过声音检测模块触发其进行切换。&#xff08;仿真图里面使用一个按键来代码声音检测模块&#xff09; 此外&#xff0c;还…

四十七、Redis分片集群

目录 一、分片集群结构 二、散列插槽 1、Redis如何判断某个key应该在哪个实例&#xff1f; 2、如何将同一类数据固定的保存在同一个Redis实例&#xff1f; 三、集群伸缩 四、故障转移 1、当集群中有一个master宕机时 &#xff08;1&#xff09;自动转移 &#xff08;2&…

使用opencv的Laplacian算子实现图像边缘检测

1 边缘检测介绍 图像边缘检测技术是图像处理和计算机视觉等领域最基本的问题&#xff0c;也是经典的技术难题之一。如何快速、精确地提取图像边缘信息&#xff0c;一直是国内外的研究热点&#xff0c;同时边缘的检测也是图像处理中的一个难题。早期的经典算法包括边缘算子方法…

Linux的文件系统 内核结构

Linux的文件系统 Q1&#xff1a;什么是文件系统&#xff1f; A&#xff1a;在学术的角度下&#xff0c;文件系统指“操作系统用于明确存储设备组织文件的方法”&#xff0c;是“文件管理系统”的简称&#xff0c;本质也是代码&#xff0c;一段程序 Q2&#xff1a;文件系统&…