当前位置: 首页 > news >正文

线段树入门 - idle

前言

笔者从2025.4.22第一次通过线段树模板,至今也不过半年时间,虽然短暂,但是却让其成为了笔者最喜欢的算法,因此,我常常会大喊我是线段树的狗。为了帮助自己记忆以及造福后人,笔者提键盘写出了这篇文章。——2025.10.29

为什么要学线段树

我认为线段树是世界上最好用的数据结构,没有之一!!!
当然,我直接这么说你是肯定不信的,让我们看看线段树都能干什么。
对于一颗线段树,它能支持:在单次时间复杂度为 \(O(\log n)\) 的情况下进行区间修改,查询。

Q:没了?
A:对,没了。

那我学个蛋,跑路了。
等等先别走,那我问你,你暴力对一个数组进行区间修改的最坏时间复杂度是不是 \(O(n)\) 的?
你说是?但是线段树可以 \(O(\log n)\) 啊,这难道真的不值得你学一下吗?
你说不值得?
我**%#*#%#
算了,闲话少说,让我们进入正题。

关于线段树的介绍

线段树长什么样?
长这样

这张图是什么意思呢?
每个方格代表一个线段树的节点。
每个节点上面的 \(id\) 代表这个点的编号。
而节点中写的 \([L,R]\) 则代表这个节点维护下标范围在 \(L\sim R\) 的区间。
例如编号为 \(5\) 的节点维护的是下标为 \(4\sim 5\) 的区间。
由观察可以得到,对于一个节点,如果它维护的区间 \([L,R]\) 满足 \(L\ne R\) 那么它一定会有两个儿子节点(我们将其称作左儿子和右儿子),如果这个点的编号为 \(id\),那么它的左儿子的编号为 \(id\times 2\),右儿子的编号为 \(id\times 2+1\)
它的左儿子维护的区间为 \([L,\lfloor \frac{L+R}{2}\rfloor]\)
它的右儿子维护的区间为 \([\lfloor \frac{L+R}{2}\rfloor+1,R]\)
知道这些基础概念之后我们就可以尝试实现一颗线段树了。
*注:下文中的 \(L,R\) 均代表当前线段树节点维护的区间的左右端点,\(mid\) 均代表 \(\lfloor \frac{L+R}{2}\rfloor\)\(l,r\) 代表查询区间(此限制对代码内的变量仍然适用)。

线段树的简单实现

我们首先要建树,现在假设我们维护长度为 \(n\) 的数组 \(a\) 的区间和。

建树

void make_tree(int L,int R,int id){if(L==R){//到达叶子结点,没有左右儿子tree[id].sum=a[L];//直接赋值return ;//退出建树函数}int mid=(L+R)>>1;make_tree(L,mid,id<<1);//递归左儿子make_tree(mid+1,R,id<<1|1);//递归右儿子tree[id].sum=tree[id<<1].sum+tree[id<<1|1].sum;//当前节点维护的区间和使用左右儿子来得到。
}

区间查询

还是刚才的图片

假设现在我们要查询区间 \([2,7]\)
那我们的答案就应用 \(id={17,9,5,12}\) 得出。
怎么实现?
我们先给代码,根据代码里面的注释进行理解。

int query(int l,int r,int L,int R,int id){if(l<=L&&r>=R) return tree[id].sum;//如果当前节点区间完全被查询区间包含,直接返回int mid=(L+R)>>1,ans=0;if(l<=mid) ans+=query(l,r,L,mid,id<<1);//如果当前区间有一部分落在在左儿子,那么递归左儿子,并将答案增加 if(r>mid/*写成r>=mid+1也可以*/) ans+=query(l,r,mid+1,R,id<<1|1);//如果当前区间有一部分落在在右儿子,那么递归右儿子,并将答案增加 return ans;//返回答案 
}

现在问题来了,如何证明这个函数是 \(O(\log n)\) 的,相信大多数初学者甚至已经学会线段树较长时间的人都不能给出一个比较完整的证明。
笔者在这里给出一个自己推出的证明方式。
首先要明确的是一颗线段树的深度是 \(\log n\) 的。
深度每增加一层,维护的区间长度会除以 \(2\)
这个很好理解。
假设查询区间为 \([l,r]\)
显然我们第一次调用函数一定会先访问节点编号为 \(1\) 的节点。
我们进行分类讨论:

  • 情况 \(1\)

\(l=1\)
如果 \(r>mid\) 那么会同时递归左右儿子,而左儿子由于被完全包含会直接在被访问时 \(return\) 掉,而递归右儿子时又会面对查询区间的右端点小于等于当前节点维护的右端点的情况,于是这个节点就会面对和它的父亲相同的情况。
如果 \(r<=mid\) 则只会递归左儿子,之后的情况就是 \(r>mid\) 的简易版了。
此时线段树的每层最坏会有两个节点被访问,时间复杂度 \(O(2\times\log n)\)

  • 情况 \(2\)

\(r=n\)
基本同上,不解释。

  • 情况 \(3\)

\(l\le mid\le r\)
我们会递归左右儿子,此时左儿子会退化成情况 \(2\),右儿子会退化成情况 \(1\)
线段树的每层最坏会有四个节点被访问,时间复杂度 \(O(4\times\log n)\)

  • 情况 \(4\)

\(l\le r\le mid\)
递归左儿子,在多次递归后迟早会退化成情况 \(1\) 或情况 \(2\) 或情况 \(3\)
线段树的每层最坏依旧会有四个节点被访问,时间复杂度 \(O(4\times\log n)\)

  • 情况 \(5\)

\(mid<l\le r\)
基本同上,不解释。
证毕。
可能写的比较抽象,但是没关系,其实你只需要知道线段树是单次查询 \(O(\log n)\) 的即可(对,其实你不明白也问题不大,但是我还是写了,也是因为笔者也因这个问题困惑了一段时间)。
区间查询就先到这里,现在就要讲修改了。

单点修改

其实学会查询后修改就没什么好说的了,直接上代码。

void add(int l,int k,int L,int R,int id){if(L==R){//到达修改的点tree[id].sum+=k; return ;}int mid=(L+R)>>1;if(l<=mid) add(l,k,L,mid,id<<1);//修改的点在左儿子 else add(l,k,mid+1,R,id<<1|1);//否则一定在右子树 tree[id].sum=tree[id<<1].sum+tree[id<<1|1].sum;//不要忘记用儿子更新自己 
}

这个代码的时间复杂度很好证明,线段树的每层一定会有一个节点被访问,时间复杂度 \(O(\log n)\)
现在我们就可以 \(AC\) P3374 【模板】树状数组 1了。
但是为什么是单点修改,不是区间修改吗,退钱!!!

http://www.sczhlp.com/news/241966/

相关文章:

  • 国外的网站叫什么网页游戏推广网站怎么做
  • 怎么查看网站哪个公司做的博兴网站建设
  • 个人网站软件海外访问国内网站 dns
  • 企业网站自助建站上海自己做的网站放在服务器哪里
  • 龙华营销型网站制作哪家好内蒙古兴安盟建设局网站
  • 天津网站建设方案维护英文外贸商城网站设计
  • 网站开发私活网站设计建设趋势
  • 设计素材网站图案免费网络营销是借助于什么营销手段
  • 如何写一份企业网站建设方案wordpress 引用视频
  • 常德网站建设网站cms二次开发网站建设
  • 建立传媒公司网站欧洲cn2 vps
  • 张店网站推广苏州城乡住房建设厅网站
  • 小程序做网站登录app定制开发报价
  • 网站开发合同缴纳印花税吗青白江区城乡和建设局网站
  • 英文网站后台维护最有效的网络推广方式
  • 百度优化 几个网站内容一样2022电商平台排行榜
  • 网站免费做链接国外视频模板网站e
  • 沙元埔做网站的公司兰州有制作网站
  • 给别人做网站如何收费四川专门做招聘酒的网站
  • 校园网站策划书wordpress导航插件
  • 做淘宝客网站要多少钱wordpress 禁止转码
  • 山东平台网站建设平台商务网站建设步骤
  • 网站开发合同管辖权异议思乐网站建设
  • 网站建设怎样设置动态背景跨境数据专线内部管理
  • 业务人员能学低代码吗
  • 低代码只能做简单表单?复杂业务场景的适配方案
  • 【题解】Educational Codeforces Round 105E
  • 大学网站栏目建设通知做网站排名大概要多少
  • 网站内链 外链Wordpress漫画插件
  • 邢台做网站咨询岳阳网站建设企业