浅谈线段树

1.前言

Oi-Wiki上的线段树

同步于 c n b l o g s cnblogs cnblogs发布。

如有错误,欢迎各位 dalao 们指出。

注:本篇文章个人见解较多,如有不适,请谅解。

前置芝士

1.二叉树的顺序储存

2.线段树是什么?

线段树,英文为 S e g m e n t Segment Segment t r e e tree tree。它是一种数据结构,主要解决区间修改和区间查询的问题。

我们用一个例题来引入线段树。

引例:

现在给定一个长度为 n n n n ≤ 1 0 6 n\le 10^6 n106)的序列 a a a。然后现在有 m m m m ≤ 1 0 6 m\le 10^6 m106)个询问操作,对于每一个操作,都有两种情况:

  • 1 x y,这一种操作表示将 a x a_x ax 加上 y y y

  • 2 l r,这一种操作表示查询 l − r l-r lr 这个区间的区间和,并输出。

对于这样的一个问题,不难发现,这是一个单点修改,区间查询的题目。

3.单点修改线段树

显然,对于刚才那道例题,我们运用暴力或者前缀和的思想在最坏情况下都会被卡到 O ( n m ) O(nm) O(nm),肯定是过不了这个题目的,如果要过这个题目,差不多也必须是在 O ( n l o g m ) O(nlogm) O(nlogm) 或者 O ( m l o g n ) O(mlogn) O(mlogn) 时间复杂度以内的算法才能够承受。因此,我们的线段树算法就此横空出世!

其实我们在学树状数组的时候,我们有一种思想就是我们通过二进制分解将一个区间化为 l o g n logn logn 个子区间。那

我们可以将 a a a 序列的 1 1 1 n n n 这个区间化成左右两个区间。为了方便举例,我们假定现在 n = 5 n=5 n=5。显然,我们可以把 [ 1 , n ] [1,n] [1,n] 化为 区间 [ 1 , 2 ] [1,2] [1,2] 和区间 [ 3 , 5 ] [3,5] [3,5]。现在定义一个区间的左右两个区间分别为这个区间的左孩子和右孩子。例如现在的区间为 [ x , y ] [x,y] [x,y],则他的左孩子就是 [ x , ( x + y ) / 2 ] [x,(x+y)/2] [x,(x+y)/2],右孩子就是 [ ( x + y ) / 2 + 1 , y ] [(x+y)/2+1,y] [(x+y)/2+1,y]。特别要注意的是如果 x = y x=y x=y,则他就是叶子节点,因为这个区间已经不能再分了。

则我们现在就可以把一个 n = 5 n=5 n=5 的序列化为一棵树。

qwq

如图,我们可以发现,所有叶子节点的区间左端点和右端点都是相等的。并且由于我们需要的是区间和,所以我们需要把书上每个区间的和的统计出来。我们可以根节点 [ 1 , 5 ] [1,5] [1,5] 这个区间编号为 1 1 1,则通过二叉树的顺序储存原理,编号为 x x x 的节点,他的左儿子为 2 x 2x 2x,右儿子为 2 x + 1 2x+1 2x+1 。为了方便查询一个区间的左儿子和右儿子,我们就运用这种顺序储存原理来储存这课树。

我们可以运用递归来进行建树。

建树参考代码如下:

struct segmentree
{int l,r;//当前节点的左端点和右端点long long data;//当前这个区间的区间和
}tree[maxe];//顺序储存,maxe 表示 4*n。(仔细想一想为什么要开4倍)
void build(int p,int l,int r)//p表示节点,l,r表示当前节点的做右端点
{if(l==r)//如果左右端点相等,就表明这个节点是叶子节点,并用继续递归下去了。{//将当前节点的三个信息储存下来。tree[p].l=l;tree[p].r=r;tree[p].data=a[l];return;}int mid=(l+r)>>1;//对他的左右儿子进行建树build(p*2,l,mid);build(p*2+1,mid+1,r);tree[p].l=l,tree[p].r=r;tree[p].data=tree[p*2].data+tree[p*2+1].data;//由于当前区间有左右孩子,所以当前区间的区间和就等于他的左右孩子的区间和之和。
}

建完树之后,接下来的问题就是修改操作。

我们可以发现,如果修改了第 x x x 位上的值,则所有包含了 x x x 的区间的区间和都要改变,也就是对一个 n = 5 , x = 3 n=5,x=3 n=5,x=3 的树, a x a_x ax 被修改了之后,我们需要将途中所有标橙了的区间的区间和修改掉。

qwq

可以发现,你无论修改的是哪一位上的数值,要被改动的区间和最坏是 ( l o g n + 1 ) (logn+1) (logn+1) (向上取整)次,所以时间复杂度为 m l o g n mlogn mlogn

对于修改操作的代码实现,仍然使用递归。

修改操作参考代码:

void change(int p,int x,int y)//p表示当前递归到的节点编号,x,y表示将a[x]改为y
{if(tree[p].l>x||tree[p].r<x)//如果当前这个节点的区间不包含x,则不需要在递归下去。return;if(tree[p].l==x&&tree[p].r==x){tree[p].data+=y;return;}//对他的孩子进行递归。change(p*2,x,y);change(p*2+1,x,y);tree[p].data=tree[p*2].data+tree[p*2+1].data;//当前区间的区间和就是他的左右儿子之和 
}

最后就是查询区间和操作。

比较容易发现,我们如果要查询 [ l , r ] [l,r] [l,r] 区间的区间和,这个区间可以在我们拆出来的二叉树中找到 l o g n logn logn 个极大区间,而我们只需要把所有拆分出来的这些区间的和给统计起来即可。时间复杂度仍然为 O ( m l o g n ) O(mlogn) O(mlogn)

区间查询参考代码:

long long ask(int p,int l,int r)//p表示当前节点编号,l,r表示要查询的[l,r]区间
{if(tree[p].r<l||tree[p].l>r)//如果当前枚举到的区间与查询的区间没有交集,则就没有必要在查询下去了return 0;if(tree[p].r<=r&&tree[p].l>=l)//如果要查询的区间包含现在美剧道德区间,则我们就直接返回这个区间的区间和{return tree[p].data;}long long res=0;res+=ask(p*2,l,r)+ask(p*2+1,l,r);//继续递归下去的时候,要把它的左右孩子递归得到的值给储存下来。return res;
}

4.线段树求区间最大最小值

上面我们讲的是线段树快速求出一个区间 [ l , r ] [l,r] [l,r] 的区间和,现在我们要讨论的就是如何运用线段树求出 [ l , r ] [l,r] [l,r] 求出一个区间的最大最小值。

我们现在直接以求最大值进行举例。

对于建树,由于是求最大值,所以我们应该将建树的倒数第二行改成求两个左右儿子节点的最大值。

建树参考代码如下:

void build(int p,int l,int r)
{if(l==r){tree[p].l=l;tree[p].r=r;tree[p].data=a[l];return;}int mid=(l+r)>>1;build(p*2,l,mid);build(p*2+1,mid+1,r);tree[p].l=l,tree[p].r=r;tree[p].data=max(tree[p*2].data,tree[p*2+1].data);//与区间和的唯一区别就在这里,是求最大值而不是求和
}

对于单点修改,我们同样需要修改倒数第二行,因为它是取最大值。特别要注意的是,如果当前递归道的区间就是 [ x , x ] [x,x] [x,x],则我们需要直接将这个区间的最大值设为 y y y,因为他的修改操作是 a x = y a_x=y ax=y

修改操作参考代码如下:

void change(int p,int x,int y)
{if(tree[p].l>x||tree[p].r<x)return;if(tree[p].l==x&&tree[p].r==x){tree[p].data=y;//直接设为yreturn;}change(p*2,x,y);change(p*2+1,x,y);tree[p].data=max(tree[p*2].data,tree[p*2+1].data);//这里是区最大值
}

最后是查询操作,这个也一样,因为是求最大值,所以我们仍然只需要改成 m a x max max 即可。

查询操作参考代码如下:

int ask(int p,int l,int r)
{if(tree[p].r<l||tree[p].l>r)return INT_MIN;if(tree[p].r<=r&&tree[p].l>=l){return tree[p].data;}int res=0;res=max(ask(p*2,l,r),ask(p*2+1,l,r));//注意是最大值return res;
}

5.线段树的区间修改以及懒惰标记

上文中,我们讲到了线段树可以维护单点修改,区间查询最大值和区间和。这时,你可能会问,那是不是线段树就不支持区间修改了呢?

Too young to simple! 线段树这么有用的数据结构怎么可能维护不了区间修改这种东西。那么接下来,就给大家介绍如何运用线段树解决区间修改的问题,

先抛出一个例题,洛谷 P3372 【模板】线段树 1。

读完题目后,你发现,这不就是线段树区间修改和区间查询的模板题吗?

对于这个题目,要用线段树做的话我们首先是建树。对于这一个建树的话没有什么可以说的,跟线段树的普通建树一样,这里不做过多阐述。

现在最头疼的就是这个区间修改操作,我们知道,如果要修改整个区间且运用普通线段树,则我们需要将所有与这个区间有交集的区间的区间和都要修改,速度最慢可以卡到 O ( n ) O(n) O(n),还没有普通的暴力快,那我们该怎么办呢?

这个时候,我们要引入一个叫做懒惰标记的东西。

我们可以对于树上的每一个节点再定义一个东西,叫做 l a z y t a g lazytag lazytag。我们运用它来储存这整个区间整体每个数被加了多少。例如我们现在将 [ 1 , 5 ] [1,5] [1,5] 这个区间全部加上 3 3 3 。则对于区间 [ 2 , 4 ] [2,4] [2,4] 来说,它的 l a z y t a g lazytag lazytag 就等于 3 3 3

当我们在递归修改时,如果当前要修改的这个区间包含了现在遍历到的节点的区间,则我们就将它的 l a z y t a g + lazytag+ lazytag+要区间加的值,并将整个区间的区间和修改。如果当前遍历到的区间只与要修改的区间存在交集,那对于普通的线段树,我们就要对他的左右儿子进行遍历。但是由于当前枚举到的这个区间的懒惰标记只记录了这个区间的,而我们还没有对它进行下传,且马上又要遍历他的子节点。所以这个时候我们就需要将这个懒惰标记下传到它的子节点身上,并修改相应的值,再进行对它儿子的递归。

修改操作参考代码如下:

void pushdown(int p)//这是懒惰标记下传操作
{if(tree[p].lazytag)//如果当前节点有懒惰标记需要下传{int l=p*2,r=p*2+1;//l,r分别表示当前节点的左右儿子long long tag=tree[p].lazytag;//tag来记录当前节点的懒惰标记tree[l].data+=(long long)(tree[l].r-tree[l].l+1)*tag;//下传的途中,我们需要将两个区间的和都进行修改,也就是加上区间长度*懒惰标记。tree[r].data+=(long long)(tree[r].r-tree[r].l+1)*tag;//同上tree[l].lazytag+=tag;//对懒惰标记进行下传tree[r].lazytag+=tag;//同上tree[p].lazytag=0;//下传完之后要将当前节点的懒惰标记清零,因为已经下传}
}
void change(int p,int l,int r,long long x)
{if(tree[p].l>r||tree[p].r<l)//如果不存在交集,就不需要再进行递归了return;if(tree[p].l>=l&&tree[p].r<=r){tree[p].data+=(long long)(tree[p].r-tree[p].l+1)*x;//区间长度呈上要修改的值tree[p].lazytag+=x;//修改懒惰标记return;}pushdown(p);//标记下传//普通线段树的递归操作change(p*2,l,r,x);change(p*2+1,l,r,x);tree[p].data=tree[p*2].data+tree[p*2+1].data; 
}

最后就是区间查询,在区间查询的时候,我们如果当前要查询的区间和枚举到的节点的区间存在交集,则我们同样需要更新他左右儿子的值并下传懒惰标记,其余的都不需要改变。

区间查询参考代码:

long long ask(int p,int l,int r)
{if(tree[p].r<l||tree[p].l>r)return 0;if(tree[p].r<=r&&tree[p].l>=l){return tree[p].data;}pushdown(p);//与普通线段树的唯一区别就是这里要下传懒惰标记和修改左右子节点的区间和,因为后面需要查询左右儿子的信息。long long res=0;res=ask(p*2,l,r)+ask(p*2+1,l,r);return res;
}

6.后记

今天对于线段树就谈到这里了,如果大家想练习线段树的话,我下面抛给大家几个例题。

P3373【模板】线段树 2

P7764 [COCI2016-2017#5] Poklon

P1168 中位数

See you next time~

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

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

相关文章

9.17UEC++代码段、编码和字符串

1. 编码定义&#xff1a; 自行转码&#xff1a; 字符串&#xff1a; FName&#xff1a;名称&#xff0c;访问快&#xff0c;用FName做键值。&#xff08;键值容器&#xff09;资产名称基本都是FName。 FText&#xff1a;一般是和UI有关&#xff0c;专门对接UI&#xff0c;也是…

JS Node 模块化解释:AMD、UMD、CommonJS、 ESM

一、前言 传统方式下&#xff0c;JS 若想引入其它 JS 文件时&#xff0c;通常使用 <script> 语法来完成&#xff0c;然而引入的 JS 往往易于造成命名污染&#xff0c;为了解决这问题&#xff0c;模块化 开发的概念逐渐浮现。 本文将以完整的 Demo 将各大模块模块的概念…

15年前的手机并没有jvm虚拟机,为何可以运行Java游戏

2000年代初期&#xff0c;随着移动通信技术的发展&#xff0c;手机逐渐普及。那个时代的手机功能相对比较单一&#xff0c;主要用于打电话和发送短信。但是&#xff0c;随着技术的进步&#xff0c;人们开始在手机上玩游戏&#xff0c;而其中最受欢迎的游戏就是Java游戏。在那个…

MoblieNetV1、V2、V3、ViT四种Moblie模型的分析对比

1、MoblieNetV1 2017年提出&#xff0c;论文地址为&#xff1a;https://arxiv.org/pdf/1704.04861.pdf 1.1 相关知识 提到了标准卷积、深度可分卷积、点卷积&#xff0c;并分析了不同卷积结构的计算量&#xff0c;&#xff08;假设 D k D_k Dk​为ksize&#xff0c;M为卷积的…

软考:中级软件设计师:存储管理,分区存储,页式存储,逻辑地址,物理地址

软考&#xff1a;中级软件设计师:存储管理&#xff0c;分区存储 提示&#xff1a;系列被面试官问的问题&#xff0c;我自己当时不会&#xff0c;所以下来自己复盘一下&#xff0c;认真学习和总结&#xff0c;以应对未来更多的可能性 关于互联网大厂的笔试面试&#xff0c;都是…

Apollo、RocketMQ加载顺序问题

在SpringCloudAlibaba框架中&#xff0c;因Nacos配置中心管理权限过于简单&#xff0c;决定用Apollo代替Nacos配置中心&#xff0c;但在启动时&#xff0c;Nacos、Redis等配置读取正常&#xff0c;RocketMQ由于启动过早&#xff0c;无法从Apollo读取自己的服务地址配置。 报错…

7.7~7.8学习总结

StringBuider&#xff1a;线程不安全&#xff0c;效率高 StringBuffer&#xff1a;线程安全&#xff0c;效率低&#xff1b; 用法举例&#xff1a; class TWC {public static void main(String []args){StringBuilder sbnew StringBuilder("小麻子爱吃粑粑");Syst…

redis的主从复制,哨兵和cluster集群

一、redis的高可用 在web服务器中&#xff0c;高可用是指服务器可以正常访问的时间&#xff0c;衡量的标准是在多长时间内可以提供正常服务(99.9%、99.99%、99.999%等等)。 高可用的计算公式是1-&#xff08;宕机时间&#xff09;/&#xff08;宕机时间运行时间&#xff09;有…

常见排序算法—面试编程题2023

常见排序算法—面试编程题2023 最近在看一些面试题&#xff0c;发现很多面试过程中都会要求手写排序编程题&#xff0c;经过一番查找整理&#xff0c;可以快速学习和使用相关排序算法题&#xff0c;通俗易懂&#xff0c;手撕代码吊打面试官。 一、冒泡排序 冒泡排序 是一种简…

C语言实现三子棋

三子棋 1. 三子棋玩法2. 程序设计思路2.1 准备工作2.2 主函数设计2.3 创建菜单界面2.4 设计棋盘2.4.1 初始化棋盘2.4.2 打印棋盘 2.5 下棋2.5.1 玩家下棋2.5.2 电脑下棋 2.6 判断输赢2.8 头文件函数声明 结束语 1. 三子棋玩法 三子棋的玩法很简单&#xff0c;两个人依次在9宫格…

【ElasticSearch】ES案例:旅游酒店搜索

文章目录 一、项目分析二、需求1&#xff1a;酒店搜索功能三、需求2&#xff1a;添加过滤功能四、需求3&#xff1a;我附近的酒店五、需求4&#xff1a;置顶花广告费的酒店 一、项目分析 启动hotel-demo项目&#xff0c;访问localhost:servicePort&#xff0c;即可访问static下…

【CANopen】周立功轻松入门CANopen笔记

前言 想学习些新东西了&#xff0c;原本想直接学学Ethercat&#xff0c;但是简单看了看对象字典啥的概念一头雾水的&#xff0c;决定先从CANopen开始&#xff0c;Ethercat看着头疼。Etehrcat和CANopen有挺多类似的地方。感谢ZLG的这个入门笔记&#xff0c;我似乎是看懂了些&am…