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;
}

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

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

相关文章

计算机网络:应用层(二) Web与http协议

我最近开了几个专栏&#xff0c;诚信互三&#xff01; > |||《算法专栏》&#xff1a;&#xff1a;刷题教程来自网站《代码随想录》。||| > |||《C专栏》&#xff1a;&#xff1a;记录我学习C的经历&#xff0c;看完你一定会有收获。||| > |||《Linux专栏》&#xff1…

编辑器Sublime text 常用快捷命令 列模式 替换空行

平替notepad 下载可取官网 www.sublimetext.com 据说可以无限试用&#xff0c;没有功能限制 1、快速删除空行 ctrl h选择正则表达式 .*Find输入&#xff1a; ^(\t)*$\nReplace输入&#xff1a;点击Replace All 2、快速选择指定字符 用鼠标选中alt f3修改 3、列编辑模式 ct…

基于ssm框架仓库系统论文

摘 要 使用旧方法对仓库信息进行系统化管理已经不再让人们信赖了&#xff0c;把现在的网络信息技术运用在仓库信息的管理上面可以解决许多信息管理上面的难题&#xff0c;比如处理数据时间很长&#xff0c;数据存在错误不能及时纠正等问题。 这次开发的仓库管理系统有管理员和…

240Wqps,美团用户中台, 如何使用DDD架构?

尼恩说在前面 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格&#xff0c;遇到很多很重要的面试题&#xff1a; 谈谈你的DDD落地经验&#xff1f; 谈谈你对DDD的理解&#x…

DCMM数据管理能力成熟度需要具备什么条件?

DCMM&#xff0c;全称为数据管理能力成熟度评估模型&#xff0c;是一个组织数据管理和应用能力的评估框架。该模型可以帮助组织清晰地定义其数据当前所处的发展阶段以及未来发展方向。具体来说&#xff0c;DCMM将数据管理能力划分为8个核心能力域&#xff1a;数据战略、数据管理…

FPGA竞赛_考试赢积分免费兑换FPGA项目课(每周更新积分排名情况)

明德扬特别组织了考试竞赛赢积分兑换FPGA项目课活动&#xff0c;欢迎大家积极参加考试&#xff01; 我是本次活动的负责人小易老师&#xff0c;有任何问题可以联系我&#xff1a;13112063618&#xff08;微信同步&#xff09; 一.考试兑换FPGA项目课 可以兑换FPGA项目课&#…

Composer 安装与使用

Composer 是 PHP 的一个依赖管理工具。我们可以在项目中声明所依赖的外部工具库&#xff0c;Composer 会帮你安装这些依赖的库文件&#xff0c;有了它&#xff0c;我们就可以很轻松的使用一个命令将其他人的优秀代码引用到我们的项目中来。 Composer 默认情况下不是全局安装&a…

微服务黑马头条(简略笔记)

Linux中nacos的拉取安装 拉取naocs镜像&#xff1a;docker pull nacos/nacos-server:1.2.0创建容器&#xff1a;docker run --env MODEstandalone --name nacos --restartalways -d -p 8848:8848 nacos/nacos-server:1.2.0访问地址&#xff1a;http://192.168.200.130:8848/n…

迈入数据结构殿堂——时间复杂度和空间复杂度

目录 一&#xff0c;算法效率 1.如何衡量一个算法的好坏&#xff1f; 2.算法效率 二&#xff0c;时间复杂度 1.时间复杂度的概念 2.大O的渐进表示法 3.推导大O的渐进表示法 4.常见时间复杂度举例 三&#xff0c;空间复杂度 一&#xff0c;算法效率 数据结构和算法是密…

LeetCode Hot100 25.K个一组翻转链表

题目&#xff1a; 给你链表的头节点 head &#xff0c;每 k 个节点一组进行翻转&#xff0c;请你返回修改后的链表。 k 是一个正整数&#xff0c;它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍&#xff0c;那么请将最后剩余的节点保持原有顺序。 你不能只是单纯…

C++相关闲碎记录(14)

1、数值算法 &#xff08;1&#xff09;运算后产生结果accumulate() #include "algostuff.hpp"using namespace std;int main() {vector<int> coll;INSERT_ELEMENTS(coll, 1, 9);PRINT_ELEMENTS(coll);cout << "sum: " << accumulate(…

软考科目如何选择?

软考科目繁多&#xff0c;让许多学弟学妹感到困惑&#xff0c;不知道该选择哪个科目。以下是一些建议&#xff0c;可以根据个人实际需求选择备考的科目。 1、初级是可选的 软考初级非常简单&#xff0c;适合刚刚入门学习的朋友报考。对于一些有基础的朋友&#xff0c;建议直接…

微信公众号(私域)的运营和变现方式

运营微信公众号也有一段时间了&#xff0c;现在将自己学习到的知识和一些心得体会分享给大家&#xff0c;希望能够对一些公众号新手有所帮助。 01 清楚公众号的变现方式 如果你注册公众号写文章不仅仅是为了记录生活、抒发感情&#xff0c;而是带着成长和赚钱的目的&#xff0…

【餐饮创业系列】创业指南

目录 一、地理位置二、菜品特色三、装修风格四、服务质量五、人力资源六、食材采购七、成本控制八、营销推广九、服务创新十、经营管理系列文章版本记录 开一间餐饮店是许多创业者的梦想&#xff0c;然而&#xff0c;要实现这个梦想并不容易。开店前&#xff0c;需要做很多准备…

FLStudio20最新2024年中文汉化版

FLStudio21.0.2.3中文版完整下载是最好的音乐开发和制作软件也称为水果循环。它是最受欢迎的工作室&#xff0c;因为它包含了一个主要的听觉工作场所。最新FL有不同的功能&#xff0c;如它包含图形和音乐音序器&#xff0c;帮助您使完美的配乐在一个美妙的方式。此程序可用于Mi…

【unity小技巧】unity最完美的CharacterController 3d角色控制器,实现移动、跳跃、下蹲、奔跑、上下坡,复制粘贴即可

最终效果 文章目录 最终效果前言为什么使用CharacterControllerSimpleMove和Move如何选择&#xff1f;1. SimpleMove2. Move 配置CharacterController参数控制相机移动跳跃下蹲处理下坡抖动问题实现奔跑和不同移速控制完整代码完结 前言 其实一开始我是不打算写的&#xff0c;…

LetNet、AlexNet、ResNet网络模型实现手写数字识别

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

欧睿 × 和鲸:联合打造 AI 中台赋能企业数字化转型,大幅提升模型产品研发效率

近年来&#xff0c;在泛零售及快消行业&#xff0c;由于市场格局越发瞬息万变、消费场景愈加错综复杂&#xff0c;以机器学习算法、人工智能模型代替纯人脑人工完成商品计划、运营、供应链管理已逐渐成为主流。 oIBP 欧睿数据&#xff08;下简称“欧睿”&#xff09;是国内领先…

java实现局域网内视频投屏播放(二)爬虫

代码链接 视频播放原理 大多视频网站使用的是m3u8&#xff0c;m3u8其实不是一个真正的视频文件&#xff0c;而是一个视频播放列表&#xff08;playlist&#xff09;。它是一种文本文件&#xff0c;里面记录了一系列的视频片段&#xff08;segment&#xff09;的网络地址。这些…

【Spring Boot】快速入门

一、引言 1、什么是spring boot&#xff1f; Spring Boot是由Pivotal团队提供的全新框架&#xff0c;其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置&#xff0c;从而使开发人员不再需要定义样板化的配置。通过这种方式&#xff…