| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 数据结构与算法 -> 算法学习模板——线段树(上) -> 正文阅读 |
|
[数据结构与算法]算法学习模板——线段树(上) |
? ? ? ? 万里之行,始于足下。本博客总结近期学习到的部分数据结构模板,以便于日后查询使用。作者水平有限,难免存在疏漏不足,恳请诸位看官斧正。倘若我的文章可以帮到你,十分荣幸。当然,笔者以后会对本文更新优化。本次内容的灵感来自于最近暑期训练对线段树内容的回顾。 目录 1.引经据礼——何谓线段树?? ? ? ?学习过树状数组的同学知道,树状数组很能够方便快捷地实现区间的前缀和查询与单点修改。但是它的本质仍然是前缀和,所以它依赖了前缀和的“前缀可减性”,单点修改操作具有一定的局限性。例如,如何维护区间最大值、最小值?而且,如果我们想更方便一点,一次修改一个区间的数呢?例如:洛谷-P3372 【模板】线段树 1 ? ? ? 于是就有了今天的主角,线段树。线段树是一种二叉搜索树,与区间树相似,它将一个区间对半划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为。 线段树示意图(来源:百度百科)? ? ? ? ?愚以为,线段树的核心思想是对于一个区间[l,r],我们可以令mid=(l+r)/2,将它分成[l,mid]和[mid+1,r]两个部分,如此分而治之,维护、查询完之后再如此递归处理它的子区间,之后不要忘记通过新的子区间来向上更新这个区间自己的信息。 2.抽丝剥茧——且听我逐层道来(1)区间节点的表示? ? ? ? 我们采用一个结构体来存储线段树的一个节点,也就是节点代表的这一段区间的数据。
? ? ? ?其中,sum是[l,r]这个区间的数据之和(因为之前我们给出的问题是快速查询区间和)。当然,如果我们需要查询其他的信息的话也需要在这里加上,比如区间乘积之类的。而lazy呢?这个关乎我们如何高效进行线段树区间修改的,后文且听我细细道来。材料有了,那我们就可以着手建立数据结构了。 (2)线段树的建树? ? ? ?线段树的建树主要采用了递归的思想。在我们在建树函数中输入线段树的根节点信息后,对半分区间,递归构建它的左右子节点。 ? ? ? ?问题来了:请思考,编号为root的二叉树左右子节点的编号是啥? ? ? ? ?数据结构知识告诉我们,在二叉树中为了便于管理,某个编号root的节点,左子节点编号为root*2,右子节点编号为root*2+1(当然,我们还可以使用位运算用root<<1表示root*2),具体原理这里不做赘述。
(3)lazy_tag? ? ? ?试想一下:在修改某个区间数的时候,如果我们对每个叶子节点逐一修改,是不是会很麻烦,违背了我们一开始想高效区间维护的初衷?我们会发现,在修改的区间中,有些子区间可能从头到尾我们查询操作都没理过它,那么,我们可以“偷懒”,只是向下更新部分叶子节点的值,而其他的区间节点仅仅只是记录我们需要修改的值,并不向下更新子节点的信息,一直到我们需要用到它,再让它向下传递,更新子节点的sum和lazy的值。 ? ? ? ?例如:我们需要让[2,8]这个区间的数都加上1 。覆盖了[2,2]、[3,4]和[5,8]这三个区间、那么我们就让这些被覆盖的区间节点lazy_tag加1,至于他们的子节点嘛,先等等看,不着急更新([2,2]这个是叶子节点,其实我们可以直接把修改的值给加上了)。当然,修改了这些区间节点,也别忘了向上更新他们的父节点信息(红色斜线标记)。 ? ? ? ?之后,我们需要查询区间[5,6]的信息,但是这里的信息还是旧的,所以这个时候我们就需要用到这个区间,我们将它的父节点的lazy_tag向下传递到左右子节点。同时也需要依据lazy_tag和父节点区间长度更新父节点,也就是更新为父节点真实的信息,并重置父节点lazy_tag值。这样我们要查询的区间就可以根据先祖节点记录的lazy_tag值来计算它的真实信息了。 ? ? ? ? 至于其他的区间如[7,8]嘛?哈哈,如果没有用到就没必要更新了,这样就省去了好多不必要操作,降低了时间复杂度。相关操作如下:
?? ? ? ?这里为了使结构更加清晰可能函数有些冗余,为了效率我们可以直接用代码替换。init_lazy、union_lazy和cal_lazy函数在之后拓展中较为复杂的线段树问题中可以更加便利地拓展。 (4)区间查询&区间修改?? ? ? 区间查询与修改操作的思路与代码是类似的。如上一节所述,我们递归修改/查询需要操作的区间的子区间,当然不要忘了向下传递lazy_tag。如果当前区间正好与需要操作的区间相同,那我们就可以很愉快地完成任务了。不然,我们需要将当前区间一分为二,到它的左右子节点递归精确查找到我们想要的区间。如果是修改的话,需要在最后向上更新父节点。
(5)总体模板代码??? ? ? 好,综上所述,我们已经分析了线段树最基础的结构了,那么它的例题代码就是如此。
??? ? ? 这里我受到牛客竞赛四系智乃的启发,采用了这一种比较“面向对象”的线段树模式,结构比较清晰合理,也易于勘误debug。 3.长虑顾后——关于线段树的一些问题的讨论(1)线段树的单点修改??? ? ? 线段树功能如此强大,那我们怎么对它进行单点修改呢?我们知道数据的单点对应着线段树的叶子结点,那么我们需要在建树的时候使用一个数组记录映射关系,即每个单点在数据(例如数组)中的序号到线段树节点编号的映射,这样我们就可以找到节点数组需要修改的位置。在修改完成之后,我们需要向上更新它所有的先祖节点,根据我们之前了解的二叉树的性质,设叶子节点编号为n,那我们就用while(n>>1)(或者说是while(n/2))对它的先祖节点遍历更新。实现代码如下:
(2)线段树多lazy_tag的后效性??? ? ? 在一些题目中,我们需要考虑的操作可能不止一种,那么我们就自然而然的想到了加入多个lazy_tag,例如牛客竞赛-数据结构,我们需要对区间进行加/乘两种不同的修改操作,设置了两个lazy_tag。但是这可能会给我们的push_down操作带来一定的麻烦: ? ? ? ? 第一步,[5,8]区间乘3,那么3号节点的乘lazy_tag改为3。第二步,[5,6]区间加1,那么6号节点的加lazy_tag改为1。问题来了:在push_down操作中,[5,6]区间的值是先加1,之后才乘3,和我们期望的顺序相反。3(x+1)和3x+1是不同的,所以在此类多lazy_tag问题中我们需要合理处理lazy_tag的后效性。 ? ? ? ??这个时候我们之前提到过的在模板例题中看似冗余的init_lazy、union_lazy和cal_lazy函数派上了用场。 定义节点
init_lazy函数? ? ? ??注意mul初值为1,因为对应的是乘法操作。
cal_lazy函数? ? ? ??利用lazy_tag计算当前点和与平方和的真实值,推导如下: ? ? ? ??其中a代表加的lazy_tag,m代表乘lazy_tag。那么我们可以得到如下代码:
? ? ? ??虽然看起来挺复杂,但是理解了线段树原理推导过程还是比较容易的。 union_lazy函数? ? ? ? 利用union_lazy合理处理lazy_tag之间的“关系”,向下传递给子节点。推导如下: ? ? ? ? 1下标为父节点,2下标为子节点(当然这里只画了一个子节点) 。得到代码如下:
? ? ? ?综上,我们可以在原有的基础题上灵活改动,处理多修改多操作问题,做到“以不变应万变”。 4.来日正长——接下来我要学习的? ? ? ?“艰难方显勇毅,磨砺始得玉成。”这次我重新捡起了一下去年学习的线段树,温故知新,收获颇丰。作为团队数据结构选手可能在接下来会巩固学习线段树的进阶操作:李超线段树、DDP等相关知识。同样的还有一些我提到的基础数据结构如前缀和、树状数组我也需要重温,所以我在中标题加入了(上)。那么感谢你能够看到最后,见证了我又一个成长的脚印。 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/25 23:41:49- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |