
离线 TA的专栏
- 打卡等级:热心大叔
- 打卡总天数:231
- 打卡月天数:0
- 打卡总奖励:4009
- 最近打卡:2025-03-13 10:31:57
|
一、问题引入
对于一般的区间问题,比如RMQ(区间的最值)、区间的和,如果使用朴素算法,即通过遍历的方式求取,则时间复杂度为O(N),在常数次查询的情况下可以接受,但是当区间长度为N,查询次数为M时,查询复杂度就变成 。在M和N较大时,这样的复杂度无法满足要求。
对于这类问题,有一个神奇的数据结构,能够在 的时间内解决问题——线段树。
二、线段树的构建
线段树的每个节点可以根据需要存储 一个区间的最大/最小值/和等内容。 它的构建方式与堆的构建方式类似,即线段树是 基于数组实现的树。
以构建区间和的线段树为例:对于给定数组 ,设大小为n,则区间范围为[0, n-1]。
- 规定线段树的根节点,即存储[0, n)的和。
- 根据堆的构建方法,父节点的左孩子为2*parent+1,右孩子为2*parent+2。
- 假设父节点存储[start, end]的和,
- mid=start+(end - start>>1)
复制代码 ,则左孩子存储[start, mid]的和,右孩子存储[mid+1, end]的和
- 注:
- mid=start+(end - start>>1)
复制代码 是一种避免整形溢出的写法,等价于。
- 由于父节点的值依赖于两个子节点,因此线段树的构建是一种后序遍历。
- // nums是给定大小为n的数组,par表示当前正在构建的线段树节点下标,start和end是当前需要计算的区间。
- void build(vector<int>& nums, int par, int start, int end)
- {
- if (start == end) // 区间大小为1,即单个点,因此当前节点的区间和就是单点的值
- {
- _segmentTree[par] = nums[start];
- return;
- }
- // 如果区间大于1,则先求当前节点的左孩子和右孩子
- int mid = start + (end - start >> 1);
- int lchild = 2 * par + 1;
- int rchild = 2 * par + 2;
- build(nums, lchild, start, mid); // 递归求左节点的区间和
- build(nums, rchild, mid + 1, end); // 递归求右孩子的区间和
- // 当前节点的值就是左孩子的值+右孩子的值
- _segmentTree[par] = _segmentTree[lchild] + _segmentTree[rchild];
- }
复制代码注:在极端情况下,最后一层有n个结点,此时线段树是一棵完全二叉树,树的高度h=log2N向上取整+1≤log2N+2。
因此,树的节点数量为2^h-1^≤2^logN+2^-1=4N-1。
所以, 线段树数组的大小一般为4*n。
此外,如果 想要避免因为n过大而导致MLE,则可以选择 来存储线段树,不过这会 增加时间成本。一般来说直接开辟4*n的线段树数组是最方便书写的。
三、线段树的单点修改与查询
1、修改
单点修改要求:修改原数组下标index处的值。此时我们需要对线段树进行更新:
- 依然是从根节点开始进行修改。
- 根据修改的下标index,判断应当修改当前节点的左子树还是右子树。
- 在递归修改左右孩子节点以后,再根据左右孩子的值重新对父节点进行赋值()。
- void update(int index, int val, int par, int start, int end)
- {
- if (start == end) // 递归结束条件依然是当前区间为单点
- {
- segtree[par] = val;
- return;
- }
- int mid = start + (end - start >> 1);
- // 递归修改左孩子或右孩子
- if (index <= mid)
- update(index, val, 2 * par + 1, start, mid);
- else
- update(index, val, 2 * par + 2, mid + 1, end);
- // 修改完成后重新对父节点赋值
- pushUp(par);
- }
- // pushUp负责利用左右孩子的值更新父节点
- void pushUp(int par)
- {
- segtree[par] = segtree[2 * par + 1] + segtree[2 * par + 2];
- }
复制代码
2、查询
由于每个节点可以存储最值和区间和,因此求最值与求和的过程几乎相同,这里以求和为例:
假设当前节点的区间为[start, end],中点为mid。
对于给定区间[left, right],它有三种分布情况:
- right<=mid,即给定区间全部在左节点中,因此只需要递归左子树计算区间和即可。
- left>mid,即给定区间全部在右节点中,因此只需要递归右子树计算区间和即可。
- 给定区间有一部分在左子树,一部分在右子树,因此需要分成两部分,一部分是[left, mid],这部分到左子树中递归求取。另一部分是[mid+1,right],这部分到右子树中递归求取。
- // [left, right]是目标求和区间,par是当前节点编号,当前节点存储区间[start, end]的和
- int query(int left, int right, int par, int start, int end)
- {
- // 目标求和区间与当前节点的区间吻合,直接返回当前节点的值即可
- if (left == start && right == end)
- return segtree[par];
- int mid = start + (end - start >> 1);
- if (right <= mid) // 目标求和区间全部在左子树
- return query(left, right, 2 * par + 1, start, mid);
- else if (left > mid) // 目标求和区间全部在右子树
- return query(left, right, 2 * par + 2, mid + 1, end);
- else // 目标求和区间分布在左右子树上
- return query(left, mid, 2 * par + 1, start, mid) +
- query(mid + 1, right, 2 * par + 2, mid + 1, end);
- }
复制代码 四、线段树的区间修改与查询
1、修改
区间修改要求:修改原数组[left, right]处的值, 将它们全部加/减value,或者 全部改为value。此时我们需要对线段树进行更新。
我们可以选择将[left, right]看成一个个点,然后进行单点修改,但是一个点的修改消耗为log2N,修改整个区间就是C*log2N了,M次修改就是M*C*log2N,这比暴力法的M*C还要差。
我们使用 懒标记法,引入一个lazy变量:
依然从根节点开始修改。
如果节点对应的区间[start, end] 完全包含在[left, right]中时,即 ,此时将这个节点的值进行修改,并按要求修改lazy,比如:对给定区间整体加4,则lazy加4,整体减3,则lazy减3。
修改完lazy数组后,我们不再需要修改它的子节点,因此lazy的意义在于减少向下更新的次数,从而降低时间复杂度**「懒的体现」**。
如果节点对应的区间[start, end] 不完全包含在[left, right]中时,则递归修改左右节点,直至对应节点的区间与待修改的区间没有交集**「递归的结束条件」**。子树修改完成后,再利用子节点的值更新父节点( )。
注意:由于lazy变量的存在,使用子节点的值更新父节点时,需要加上父节点的lazy值,因为该值是由于"偷懒"而没有添加在子节点上的。 - // 以「将给定区间内的数加x,查询每个节点存储对应区间的和」为例:
- void update(int left, int right, int x, int node, int start, int end)
- {
- // 区间没有交集,无需修改
- if (end < left || right < start)
- return;
- // 当前节点对应的区间被需要修改的区间完全包含
- if (left <= start && right >= end)
- {
- segtree[node].val += x * (end - start + 1);
- segtree[node].lazy += x;
- return;
- }
- // 不被[left, right]完全包含,则说明本轮只会更新[start, end]的一部分,因此不能再"偷懒"直接将x加在lazy上了
- // 而是先根据lazy的值修改左右子节点,然后再递归修改左右子树
- int mid = start + ((end - start) >> 1);
- // 先利用lazy修改孩子节点
- pushDown(node, mid - start + 1, end - mid);
- // 递归修改孩子节点
- update(left, right, 2 * node + 1, start, mid);
- update(left, right, 2 * node + 2, mid + 1, end);
- // 利用左右子树的区间最大值确定父节点的区间最大值
- pushUp(par);
- }
- void pushUp(int par)
- {
- segtree[par].val = segtree[2 * par + 1] + segtree[2 * par + 2] + segtree[par].lazy;
- }
- // par表示父节点,ln表示左孩子的区间长度,rn表示右孩子的区间长度
- void pushDown(int par, int ln, int rn)
- {
- if (segtree[par].lazy != 0)
- {
- segtree[2 * par + 1].val += segtree[par].lazy * ln; // 修改左孩子的值
- segtree[2 * par + 1].lazy += segtree[par].lazy; // 偷懒,不再往下继续修改,因此左孩子继承父节点的lazy值
- segtree[2 * par + 2].lazy += segtree[par].lazy * rn;
- segtree[2 * par + 2].lazy += segtree[par].lazy;
- segtree[par].lazy = 0; // 父节点的lazy已经分配到子节点了,因此父节点lazy清零
- }
- }
复制代码 2、查询
查询的过程与修改几乎相同:
- 依然从根节点开始查询。
- 如果当前节点有懒标记,此时返回节点的值,无需向下遍历。
- 当某个节点对应的区间[start, end]完全包含在[left, right]中时,即,则该节点的值是我们最终结果的子集,直接返回节点值即可。
- 如果不完全包含,则递归查询左右子树,直至对应节点的区间与待修改的区间没有交集**「递归的结束条件」**。利用子树的查询结果作为最终的返回结果。
- // 以「将给定区间内的数加x,查询每个节点存储对应区间的和」为例:
- bool query(int left, int right, int node, int start, int end)
- {
- // 区间没有交集,无需查询
- if (end < left || right < start)
- return false;
- // 有懒标记,则无需查询左右孩子,而是直接返回节点值,外加懒标记
- // 或者当前节点对应的区间被需要查询的区间完全包含,则直接返回节点值
- if (segtree[node].lazy || left <= start && right >= end)
- return segtree[node].val;
- int mid = start + ((end - start) >> 1);
- // 不完全包含,则先根据lazy修改子节点,再递归查询左右子树的和
- pushDown(node, mid - start + 1, end - mid);
- return query(left, right, 2 * node + 1, start, mid) +
- query(left, right, 2 * node + 2, mid + 1, end);
- }
- // par表示父节点,ln表示左孩子的区间长度,rn表示右孩子的区间长度
- void pushDown(int par, int ln, int rn)
- {
- if (segtree[par].lazy != 0)
- {
- segtree[2 * par + 1].val += segtree[par].lazy * ln; // 修改左孩子的值
- segtree[2 * par + 1].lazy += segtree[par].lazy; // 偷懒,不再往下继续修改,因此左孩子继承父节点的lazy值
- segtree[2 * par + 2].lazy += segtree[par].lazy * rn;
- segtree[2 * par + 2].lazy += segtree[par].lazy;
- segtree[par].lazy = 0; // 父节点的lazy已经分配到子节点了,因此父节点lazy清零
- }
- }
复制代码以上就是C++ 线段树原理与实现示例详解的详细内容,更多关于C++ 线段树原理的资料请关注晓枫资讯其它相关文章!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:  进行删除处理。
4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
|