《林林数据结构笔记》线段树求数组区间和,单点更新,区间更新+lazy思想
昨天刷力扣认识的这个数据结构,原题在这里。记录一下力扣https://leetcode.cn/problems/my-calendar-iii/
例题
给定数组[1, 2, 3, 4, 5, ....],编写函数来返回数组任意区间元素的和
朴素的想法
直接把每个元素相加,时间复杂度为O(n),n是区间长度
def getSum(arr, start, end):val = 0for i in range(start, end + 1):val += arr[i]return val
前缀和算法
前缀和是一种常用的、较为高效的预处理方式。能够有效降低查询的时间复杂度。前缀和可以理解为数组前项的和。查询的时间复杂度为O(1)
class PartialSumArr:def __init__(self, arr):val = 0self.partialSumArr = []for i in arr:val += iself.partialSumArr.append(val)def getSum(self, start, end):if start == 0:return self.partialSumArr[end]return self.partialSumArr[end] - self.partialSumArr[start-1]partialSumArr = PartialSumArr([1, 2, 3, 4, 5, 6, 7])
print(partialSumArr.partialSumArr)
print(partialSumArr.getSum(0, 4))
print(partialSumArr.getSum(3, 6))>>>[1, 3, 6, 10, 15, 21, 28]
15
22
新的问题
如果要修改数组中某一项,即更新数组。
对于方法一:更新的时间复杂度为O(1);
对于方法二:更新的时间复杂度为O(n),因为需要把前缀和数组中这个元素之后每个元素都更新一遍
如果要实现区间更新呢?
对于方法一:更新的时间复杂度为O(n);n为区间大小
对于方法二:更新的时间复杂度为O(m*n),m为区间最右下标位置距离数组最右距离,n为区间长度
线段树
线段树是一种数据结构,可以有效地对数组进行范围内的查询,同时可以足够灵活地修改数组。
给定一个数组(arr),编写函数来求其任意区间元素和
建树
这是根节点
通过求两个下标的平均值来确定其子节点
同理
至此我们的树就建立好了,下面创建一个数组(nodes)来储存树的节点。当父节点为node[index]时,我们把它的子节点放在node[index * 2]和node[index * 2 + 1]位置。我们通常把根节点放在下标1的位置。如下图,数组里0, 4表示下标0~4的元素和,这么写方便和上面树图对比
替换成对应值
对应代码:
class SegmentTreeSum:def __init__(self, arr):self.arr = arrself.nodes = [0] * len(arr) * 2self.construct(1, 0, len(arr) - 1)def construct(self, index, left, right):# left = right,确定节点if left == right:self.nodes[index] = self.arr[left]else:mid = (left + right) // 2 # 取整# 递归先确定子节点,再把子节点值相加获取该节点的值self.construct(index * 2, left, mid) # 左节点self.construct(index * 2 + 1, mid + 1, right) # 右节点self.nodes[index] = self.nodes[index * 2] + self.nodes[index * 2 + 1] segmentTreeSum = SegmentTreeSum([1, 2, 3, 4, 5])
print(segmentTreeSum.nodes)>>>[0, 15, 6, 9, 3, 3, 4, 5, 1, 2]
查询
index表示nodes下标,start和end表示查询区间,left和right表示nodes[index]表示的区间范围
def findSum(index, start, end, left, right):从根节点开始查询:findSum(1, start, end, 0, len(arr)-1)
查询需要考虑三种情况
1. [start, end]在[left, right]之外,没有符合元素,返回0
2. [start, end]和[left, right]表示区间相同,直接返回nodes[index]
3. [start, end]和[left, right]表示区间不同
比如我们从根节点开始查询,两个区间不同,接下来通过递归对两个子树进行查询,再把它们结果相加
下图可见,两个区间不同,继续递归
下图可见, 查询区间超出范围,right = 1 < start = 2,返回0
查询另一个子树,区间不同,但是该节点没有子树无法继续递归
同理当查询到(3, 3)和(4, 4)节点时也虽然满足条件,但也无法继续递归 。我们发现把这三个节点相加得到的数就是我们需要的[2, 4]区间元素和。所以当left = right即没有子树时,我们返回nodes[index]
代码部分:时间复杂度为O(logN)
class SegmentTreeSum:...def getSum(self, start, end):return self.findSum(1, start, end, 0, len(self.arr) - 1)def findSum(self, index, start, end, left, right):if end < left or start > right:return 0elif left == right:return self.nodes[index]else:if start == left and end == right:return self.nodes[index]mid = (left + right) // 2leftNode = self.findSum(index * 2, start, end, left, mid)rightNode = self.findSum(index * 2 + 1, start, end, mid + 1, right)return leftNode + rightNodesegmentTreeSum = SegmentTreeSum([1, 2, 3, 4, 5])
print(segmentTreeSum.getSum(2, 4))
单点更新
i表示原数组arr更新下标
def change(index, i, val, left, right):从根节点开始查询:findSum(1, start, end, 0, len(arr)-1)
和查询类似
1. left = i 且 right = i,说明找到该节点,直接更新 nodes[index] = val
2. i < left or i > right,超出范围
3. 没有超出范围,递归更新子树
代码部分:时间复杂度为O(logN)
class SegmentTreeSum:...def update(self, i, val):self.change(1, i, val, 0, len(self.arr) - 1)def change(self, index, i, val, left, right):if left == i and right == i:self.nodes[index] = valelif i < left or right < i:returnelse:mid = (left + right) // 2self.change(index * 2, i, val, left, mid)self.change(index * 2, i, val, mid + 1, right)self.nodes[index] = self.nodes[index * 2] + self.nodes[index * 2 + 1]segmentTreeSum = SegmentTreeSum([1, 2, 3, 4, 5])
print(segmentTreeSum.nodes)
segmentTreeSum.update(2, 6)
print(segmentTreeSum.nodes)>>>[0, 15, 6, 9, 3, 3, 4, 5, 1, 2]
[0, 18, 9, 9, 6, 3, 4, 5, 1, 2]
区间更新(lazy算法)
所谓lazy算法就是在更新时,只更新要求更新的这一段,而不更新他的子节点。当我们要访问他的子节点时,我们才去更新他的值。这样我们就能减少很多更新的次数,从而减少时间。
创建lazy数组
self.lazy = [0] * len(arr) * 2
val是区间内每个元素增加的值
def changeRange(self, index, start, end, left, right, val):
和上面情况类似
1. [start, end]在[left, right]之外,不用进行操作
2. [start, end]和[left, right]表示区间相同,只更新nodes[index]和lazy[index],不用把区间内每个元素都更新。
3. [start, end]和[left, right]表示区间不相同,递归子节点
写到这里我感觉我已经把线段树看透了,写上面的时候还有一知半解的地方。突然感觉这东西这么简单真的需要人教吗?不废话了,直接上代码。时间复杂度O(logN)
class SegmentTreeSum:...self.lazy = [0] * len(arr) * 2...def updateRange(self, start, end, val):self.changeRange(1, start, end, 0, len(self.arr) - 1, val)def changeRange(self, index, start, end, left, right, val):if end < left or start > right:returnelif start == left and end == right:self.nodes[index] += (end - start + 1) * valself.lazy[index] += valelse:if left == right:self.nodes[index] += valself.lazy[index] += valreturnmid = (left + right) // 2self.changeRange(index * 2, start, end, left, mid, val)self.changeRange(index * 2 + 1, start, end, mid + 1, right, val)self.nodes[index] = self.nodes[index * 2] + self.nodes[index * 2 + 1]segmentTreeSum = SegmentTreeSum([1, 2, 3, 4, 5])
print(segmentTreeSum.nodes)
segmentTreeSum.updateRange(0, 4, 3)
print(segmentTreeSum.nodes)>>>[0, 15, 6, 9, 3, 3, 4, 5, 1, 2]
[0, 30, 6, 9, 3, 3, 4, 5, 1, 2]
需要注意的地方
我们看这段测试
segmentTreeSum = SegmentTreeSum([1, 2, 3, 4, 5])
segmentTreeSum.updateRange(0, 4, 3)
print(segmentTreeSum.nodes)
print(segmentTreeSum.lazy)
print(segmentTreeSum.getSum(1, 2))>>>[0, 30, 6, 9, 3, 3, 4, 5, 1, 2]
[0, 3, 0, 0, 0, 0, 0, 0, 0, 0]
5
答案很明显是错的,而且错误原因也很明显。上面说lazy算法,当我们要访问他的子节点时,我们才去更新他的值。所以当我们求和时我们首先需要去更新它的值
添加pushTree()方法来更新子节点
class SegmentTreeSum:...def pushTree(self, index, val):if index * 2 < len(self.nodes):self.nodes[index * 2] += valself.nodes[index * 2 + 1] += valself.pushTree(index * 2, val)self.pushTree(index * 2 + 1, val)
更新求和函数
def findSum(self, index, start, end, left, right):if self.lazy[index] != 0:self.pushTree(index, self.lazy[index])if end < left or start > right:return 0elif left == right:return self.nodes[index]else:if start == left and end == right:return self.nodes[index]mid = (left + right) // 2leftNode = self.findSum(index * 2, start, end, left, mid)rightNode = self.findSum(index * 2 + 1, start, end, mid + 1, right)return leftNode + rightNode
完整代码
class SegmentTreeSum:def __init__(self, arr):self.arr = arrself.nodes = [0] * len(arr) * 2self.lazy = [0] * len(arr) * 2self.construct(1, 0, len(arr) - 1)# 建树def construct(self, index, left, right):# left = right,确定节点if left == right:self.nodes[index] = self.arr[left]else:mid = (left + right) // 2 # 取整# 递归先确定子节点,再把子节点值相加获取该节点的值self.construct(index * 2, left, mid) # 左节点self.construct(index * 2 + 1, mid + 1, right) # 右节点self.nodes[index] = self.nodes[index * 2] + self.nodes[index * 2 + 1] # 相加# 更新节点def pushTree(self, index, val):if index * 2 < len(self.nodes):self.nodes[index * 2] += valself.nodes[index * 2 + 1] += valself.pushTree(index * 2, val)self.pushTree(index * 2 + 1, val)# 获取区间和def getSum(self, start, end):return self.findSum(1, start, end, 0, len(self.arr) - 1)def findSum(self, index, start, end, left, right):if self.lazy[index] != 0:self.pushTree(index, self.lazy[index])if end < left or start > right:return 0elif left == right:return self.nodes[index]else:if start == left and end == right:return self.nodes[index]mid = (left + right) // 2leftNode = self.findSum(index * 2, start, end, left, mid)rightNode = self.findSum(index * 2 + 1, start, end, mid + 1, right)return leftNode + rightNode# 单点更新def update(self, i, val):self.change(1, i, val, 0, len(self.arr) - 1)def change(self, index, i, val, left, right):if left == i and right == i:self.nodes[index] = valelif i < left or right < i:returnelse:mid = (left + right) // 2self.change(index * 2, i, val, left, mid)self.change(index * 2, i, val, mid + 1, right)self.nodes[index] = self.nodes[index * 2] + self.nodes[index * 2 + 1]# 区间更新def updateRange(self, start, end, val):self.changeRange(1, start, end, 0, len(self.arr) - 1, val)def changeRange(self, index, start, end, left, right, val):if end < left or start > right:returnelif start == left and end == right:self.nodes[index] += (end - start + 1) * valself.lazy[index] += valelse:if left == right:self.nodes[index] += valself.lazy[index] += valreturnmid = (left + right) // 2self.changeRange(index * 2, start, end, left, mid, val)self.changeRange(index * 2 + 1, start, end, mid + 1, right, val)self.nodes[index] = self.nodes[index * 2] + self.nodes[index * 2 + 1]
参考网站:
Segment Tree - Algorithms for Competitive Programminghttps://cp-algorithms.com/data_structures/segment_tree.html力扣https://leetcode.cn/problems/my-calendar-iii/solution/xian-duan-shu-by-xiaohu9527-rfzj/前缀和 - 知乎前缀和(Partial_sum)简介(Introduction) 前缀和是一种常用的、较为高效的预处理方式。能够有效降低查询的时间复杂度。前缀和可以理解为数组前n项的和。 一维前缀和示例(Example) 数列:\begin{matrix} 1&2&…https://zhuanlan.zhihu.com/p/350910138线段树(lazy用法)_起烟的博客-CSDN博客_线段树lazy在上一篇中,我们讨论了线段树的基础用法,其中我们对于线段树的修改,仅仅限制于对于线段树的点的修改,而不是对于某一个一段区间的修改。那么我们现在来想想如果对于线段树的一段区间来进行修改的话,如果我们还是来对线段树的每一个点都进行修改的话,那么,假设我们需要修改m个点,其时间复杂度岂不是为O(log(n))了,那么我们所需要的少的时间复杂度的要求就没有达成,为此,lazy数组应运而生,来解决这个问题。首先我们来认识一下lazy:如果当前区间被需要修改的目标区间完全覆盖,打一个标记。如果下一次的查询或者更改包https://blog.csdn.net/malloch/article/details/107999709
以上为我的笔记,接受友好地建议和指正~
《林林数据结构笔记》线段树求数组区间和,单点更新,区间更新+lazy思想相关推荐
- 【学习笔记】线段树的数组大小下限及证明
手动博客搬家: 本文发表于20170820 20:23:52, 原地址https://blog.csdn.net/suncongbo/article/details/77432667 线段树是一种将一 ...
- 307. Range Sum Query - Mutable | 307. 区域和检索 - 数组可修改(数据结构:线段树,图文详解)
题目 https://leetcode.com/problems/range-sum-query-mutable/ 吐槽官方题解 这题的 英文版官方题解,配图和代码不一致,而且描述不清:力扣国内版题解 ...
- 线段树的数组大小下限及证明
线段树的数组大小下限及证明 手动博客搬家: 本文发表于20170820 20:23:52, 原地址https://blog.csdn.net/suncongbo/article/details/774 ...
- c语言线段树建树程序,c语言数据结构之线段树详解;例题:校门外的树(poj2808或者vijos1448)...
线段树:它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个结点. 也就是说线段树的每一个结点对应一个区间,其中根节点对应区间[1,n] 对于线段树中的每一个非叶子节点[a,b],它的左儿子 ...
- hdu 2461(线段树求面积并)
题意:给出N个矩形,M次询问 每次询问给出R个,问这R个矩形围成的面积 解题思路:对于每次询问,做一次线段树求面积的并操作. 每个节点保存的信息有l,r,cover,len分别表示该节点表示的区间[l ...
- 线段树求逆序对(hdu1394Minimum Inversion Number)
说实话,线段树求逆序对我理解了半天诶,不知是否有人像我一样. 对于每个数来说,只有和已经出现过的.比它大的数才能形成逆序对,那么在给定的数列中,每给一个数就向前找比它大的数. 样例:10 1 3 6 ...
- 吉首大学2019年程序设计竞赛(重现赛) 干物妹小埋(线段树求最长上升子序列)
链接:https://ac.nowcoder.com/acm/contest/992/B 来源:牛客网 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 32768K,其他语言65536 ...
- HDU - 1255 覆盖的面积(线段树求矩形面积交 扫描线+离散化)
链接:线段树求矩形面积并 扫描线+离散化 1.给定平面上若干矩形,求出被这些矩形覆盖过至少两次的区域的面积. 2.看完线段树求矩形面积并 的方法后,再看这题,求的是矩形面积交,类同. 求面积时,用被覆 ...
- poj 1151(线段树求面积并)
解题思路:线段树求面积并,水题 #include<iostream> #include<cstdio> #include<cstring> #include< ...
- uscao 线段树成段更新操作及Lazy思想(POJ3468解题报告)
线段树成段更新操作及Lazy思想(POJ3468解题报告) 标签: treequerybuildn2cstruct 2011-11-03 20:37 5756人阅读 评论(0) 收藏 举报 分类: ...
最新文章
- BarTender安装常见问题集结
- windbg断点学习总结
- C语言 | 卡尔曼滤波算法2——算法分析
- FATAL: NO bootable medium found! System halted
- kali下sqliv:SQL注入URL扫描器
- java跟踪会话_JavaWeb会话跟踪
- 【架构设计】Android:配置式金字塔架构
- Codeforces Round #236 (Div. 2) C. Searching for Graph(水构造)
- paip.python错误解决15
- Android学习资源网站 1
- vfp报表纸张设置_VFP报表输出.doc
- 系统架构设计师考试心得与经验
- Dynamics CRM 2013学习伊始
- vue移动端复杂表格表头,固定表头与固定第一列
- bignumber.js API
- Visual Studio.net 2003 安装和卸载的教训(包括软件下载地址及安装方法)
- 侍魂胧月传说辅助【有八鸽】侍魂胧月传说全能免费辅助脚本功能和使用说明
- Dell服务器组建阵列-Raid(无阵列卡)
- 微软常用运行库合集(32+64位)
- 解决Sklearn ValueError: empty vocabulary; perhaps the documents only contain stop words