线段树广泛应用于解决各种问题,例如动态规划、区间覆盖、离散化等。它提供了一种高效且灵活的方式来处理区间操作,使得许多复杂的问题可以被简化和解决。
概要
线段树是一种重要的数据结构,它在解决各种问题中都有着广泛的应用。以下是一些常见的线段树应用:
-
区间查询和更新:线段树最常见的应用之一是在数组或序列上进行区间查询和更新操作。例如,可以使用线段树来高效地计算给定区间内的最大值、最小值、和、平均值等。
-
离散化和区间覆盖:线段树可以用于离散化处理,将一个区间内的数值映射到一个较小的范围内。此外,线段树还可以用于管理区间的覆盖情况,例如查找覆盖某个区间的最小子区间。
-
区间交集和合并:线段树可以用于查找多个区间之间的交集或合并操作,这在处理区间重叠等问题时非常有用。
-
统计区间内满足特定条件的元素个数:线段树可以帮助我们高效地统计给定区间内满足特定条件的元素个数,比如统计区间内大于某个阈值的元素个数。
-
动态规划中的区间DP:在线段树的基础上,可以实现一些复杂的动态规划算法,例如区间DP问题,通过线段树来维护状态转移过程中的区间信息。
什么叫做线段树
线段树(Segment Tree)是一种用于高效处理区间查询和更新操作的数据结构。它可以将一个数组或序列表示为一棵二叉树,每个节点代表数组中的一个区间,并存储该区间的一些统计信息。
线段树的构建过程是一个递归的过程。首先,将数组划分成两个子区间,然后递归地构建左子树和右子树,直到区间只包含一个元素。在构建过程中,每个节点都会保存其所代表的区间的一些统计信息,例如最大值、最小值、和、平均值等。
线段树的主要优势在于它可以在O(logN)的时间复杂度内进行区间查询和更新操作。对于区间查询,可以通过递归地查找目标区间所涉及的子区间,并综合子区间的统计信息得出结果。对于区间更新,可以通过递归地更新目标区间所涉及的子区间的统计信息来实现。
带lazy标签的线段树各功能过程
一种问题的情况:比如:区间加法: 对于[L,R]的区间,它的答案可以由[L,M]和[M+1,R]的答案合并求出 满足的问题:区间求和,区间最大值最小值等 主要解决方法流程: 1、建树 2、单点修改、区间修改(需要用到额外的变量) 3、区间查询 !一般而言,位运算代码效率更高,而且对于线段树的数组,一般都要开到4*n才能满足。
写在前面:
建树:
def build_tree(self, root, start, end, nums):
# 递归构建线段树
if start == end:
self.tree[root] = nums[start]
return
mid = (start + end) // 2
self.build_tree(2 * root + 1, start, mid, nums) # 递归构建左子树
self.build_tree(2 * root + 2, mid + 1, end, nums) # 递归构建右子树
self.tree[root] = self.tree[2 * root + 1] + self.tree[2 * root + 2] # 合并左右子树的值到当前节点
lazy标签的更新:
def propagate_lazy(self, root, start, end):
# 将延迟更新的标记向下传递
if self.lazy[root] != 0: # 如果当前节点有延迟标记
self.tree[root] += self.lazy[root] * (end - start + 1) # 更新当前节点的值
if start != end: # 如果不是叶子节点
self.lazy[2 * root + 1] += self.lazy[root] # 将延迟标记下传给左子节点
self.lazy[2 * root + 2] += self.lazy[root] # 将延迟标记下传给右子节点
self.lazy[root] = 0 # 清空当前节点的延迟标记
1、 修改数列中,下标为i的数据,从根节点向下深度搜索:
如果当前节点的左儿子的区间[L,R]包含了i ,也就是L<=i<=R ,就访问左儿子,否则就右儿子
直到L=R,也就是搜到了只包含这个数据的节点,就可以修改它
最重要的是:!!!不要忘记将包含此数据的大区间的值进行更新 (修改父亲节点的值)
def point_update_with_lazy(self, root, start, end, index, val):
# 单个点的更新
self.propagate_lazy(root, start, end) # 在执行单个点更新前,先将当前节点的延迟标记向下传递
if start == end: # 如果当前节点是叶子节点
self.tree[root] = val # 更新当前节点的值
return
mid = (start + end) // 2
if index <= mid:
self.point_update_with_lazy(2 * root + 1, start, mid, index, val) # 递归更新左子树
else:
self.point_update_with_lazy(2 * root + 2, mid + 1, end, index, val) # 递归更新右子树
self.tree[root] = self.tree[2 * root + 1] + self.tree[2 * root + 2] # 更新当前节点的值
2、查询的区间,返回区间和:
只需要判断它跟哪一个区间有关系,就深度其子孩子
如果要查询的区间完全覆盖当前区间,直接返回当前区间的值
如果查询区间和左儿子有交集,搜索左儿子
如果查询区间和右儿子有交集,就搜索右儿子
最后 合并处理两边查询的数据
总结起来就是:1、覆盖?(当前的区间,是否被要查询的区间覆盖了) 2、左还是右 3、合并
def modified_range_query_with_lazy(self, root, start, end, left, right):
# 带延迟更新的区间查询
self.propagate_lazy(root, start, end) # 在执行区间查询前,先将当前节点的延迟标记向下传递
if left <= start and end <= right:
return self.tree[root] # 如果当前节点表示的区间完全在待查询区间内,则返回当前节点的值
mid = (start + end) // 2
res = 0
if left <= mid:
res += self.modified_range_query_with_lazy(2 * root + 1, start, mid, left, min(right, mid)) # 递归查询左子树
if right > mid:
res += self.modified_range_query_with_lazy(2 * root + 2, mid + 1, end, max(left, mid + 1), right) # 递归查询右子树
return res # 返回查询结果
3、区间修改: 如果按照常规的思路,向下递归遍历所有节点并且一一修改,时间复杂度和暴力处理相差无几 。
所以这个地方就需要用到lazy标记(懒标记),
将此区间标记,表示这个区间的值已经更新,但它的子区间却没有跟新,更新的信息就是标记里存的值 即:
1、如果要修改的区间完全覆盖当前区间,直接更新这个区间。打上lazy标记
2、如果没有完全覆盖,且当前区间有lazy标记,先下传lazy标记到子区间,再清除当前区间的lazy标记(多次修改的时候)
3、如果修改区间和左儿子有交集,搜索左儿子
4、如果修改区间和右儿子有交集,就搜索右儿子
最后将当前区间的值进行更新
总结起来就是:1、覆盖?lazy 2、下传 清除 3、左/右 4、更新
def range_update_with_lazy(self, root, start, end, left, right, val):
# 带延迟更新的区间修改
self.propagate_lazy(root, start, end) # 在执行区间修改前,先将当前节点的延迟标记向下传递
if left <= start and end <= right: # 如果当前节点表示的区间完全在待更新区间内
self.tree[root] += val * (end - start + 1) # 直接更新当前节点的值
if start != end: # 如果不是叶子节点
self.lazy[2 * root + 1] += val # 将更新标记下传给左子节点
self.lazy[2 * root + 2] += val # 将更新标记下传给右子节点
return
mid = (start + end) // 2
if left <= mid:
self.range_update_with_lazy(2 * root + 1, start, mid, left, min(right, mid), val) # 递归更新左子树
if right > mid:
self.range_update_with_lazy(2 * root + 2, mid + 1, end, max(left, mid + 1), right, val) # 递归更新右子树
self.tree[root] = self.tree[2 * root + 1] + self.tree[2 * root + 2] # 更新当前节点的值
4、区间修改的区间查询:
1、如果要查询的区间完全覆盖当前区间,直接返回当前区间的值
2、如果没有被完全包含,下传lazy标记(相比于单值查询多了这一步)
3、如果查询区间和左儿子有交集,就搜索左儿子
4、如果查询区间和右儿子有交集,就搜索右儿子
最后合并处理两边查询的数据
总结起来就是:1、覆盖? 2、下传 3、 左/右 4、合并
def modified_range_query_with_lazy(self, root, start, end, left, right):
# 带延迟更新的区间查询
self.propagate_lazy(root, start, end) # 在执行区间查询前,先将当前节点的延迟标记向下传递
if left <= start and end <= right:
return self.tree[root] # 如果当前节点表示的区间完全在待查询区间内,则返回当前节点的值
mid = (start + end) // 2
res = 0
if left <= mid:
res += self.modified_range_query_with_lazy(2 * root + 1, start, mid, left, min(right, mid)) # 递归查询左子树
if right > mid:
res += self.modified_range_query_with_lazy(2 * root + 2, mid + 1, end, max(left, mid + 1), right) # 递归查询右子树
return res # 返回查询结果
python线段树的模板
class SegmentTreeWithLazyPropagation:
def __init__(self, n):
# 初始化线段树数组和延迟标记数组,长度为4*n,其中n为原始数据的长度
self.tree = [0] * (4 * n)
self.lazy = [0] * (4 * n)
def build_tree(self, root, start, end, nums):
# 递归构建线段树
if start == end:
self.tree[root] = nums[start]
return
mid = (start + end) // 2
self.build_tree(2 * root + 1, start, mid, nums) # 递归构建左子树
self.build_tree(2 * root + 2, mid + 1, end, nums) # 递归构建右子树
self.tree[root] = self.tree[2 * root + 1] + self.tree[2 * root + 2] # 合并左右子树的值到当前节点
def propagate_lazy(self, root, start, end):
# 将延迟更新的标记向下传递
if self.lazy[root] != 0: # 如果当前节点有延迟标记
self.tree[root] += self.lazy[root] * (end - start + 1) # 更新当前节点的值
if start != end: # 如果不是叶子节点
self.lazy[2 * root + 1] += self.lazy[root] # 将延迟标记下传给左子节点
self.lazy[2 * root + 2] += self.lazy[root] # 将延迟标记下传给右子节点
self.lazy[root] = 0 # 清空当前节点的延迟标记
def range_update_with_lazy(self, root, start, end, left, right, val):
# 带延迟更新的区间修改
self.propagate_lazy(root, start, end) # 在执行区间修改前,先将当前节点的延迟标记向下传递
if left <= start and end <= right: # 如果当前节点表示的区间完全在待更新区间内
self.tree[root] += val * (end - start + 1) # 直接更新当前节点的值
if start != end: # 如果不是叶子节点
self.lazy[2 * root + 1] += val # 将更新标记下传给左子节点
self.lazy[2 * root + 2] += val # 将更新标记下传给右子节点
return
mid = (start + end) // 2
if left <= mid:
self.range_update_with_lazy(2 * root + 1, start, mid, left, min(right, mid), val) # 递归更新左子树
if right > mid:
self.range_update_with_lazy(2 * root + 2, mid + 1, end, max(left, mid + 1), right, val) # 递归更新右子树
self.tree[root] = self.tree[2 * root + 1] + self.tree[2 * root + 2] # 更新当前节点的值
def point_update_with_lazy(self, root, start, end, index, val):
# 单个点的更新
self.propagate_lazy(root, start, end) # 在执行单个点更新前,先将当前节点的延迟标记向下传递
if start == end: # 如果当前节点是叶子节点
self.tree[root] = val # 更新当前节点的值
return
mid = (start + end) // 2
if index <= mid:
self.point_update_with_lazy(2 * root + 1, start, mid, index, val) # 递归更新左子树
else:
self.point_update_with_lazy(2 * root + 2, mid + 1, end, index, val) # 递归更新右子树
self.tree[root] = self.tree[2 * root + 1] + self.tree[2 * root + 2] # 更新当前节点的值
def modified_range_query_with_lazy(self, root, start, end, left, right):
# 带延迟更新的区间查询
self.propagate_lazy(root, start, end) # 在执行区间查询前,先将当前节点的延迟标记向下传递
if left <= start and end <= right:
return self.tree[root] # 如果当前节点表示的区间完全在待查询区间内,则返回当前节点的值
mid = (start + end) // 2
res = 0
if left <= mid:
res += self.modified_range_query_with_lazy(2 * root + 1, start, mid, left, min(right, mid)) # 递归查询左子树
if right > mid:
res += self.modified_range_query_with_lazy(2 * root + 2, mid + 1, end, max(left, mid + 1), right) # 递归查询右子树
return res # 返回查询结果
# 创建对象
seg_tree = SegmentTreeWithLazyPropagation(5)
# 构建线段树
nums = [1, 3, 5, 7, 9]
seg_tree.build_tree(0, 0, 4, nums)
# 区间修改:将索引为 1 到 3 的元素都加上 2
seg_tree.range_update_with_lazy(0, 0, 4, 1, 3, 2)
# 对第二个树进行更新,更新为4
seg_tree.point_update_with_lazy(0,0,4,1,4)
# nums = [1, 4, 7, 9, 9]
# 区间查询:查询索引为 1 到 4 的元素的和
result = seg_tree.modified_range_query_with_lazy(0, 0, 4, 1, 4)
print(result) # 输出结果为 29
小结
今天我学习了关于线段树(Segment Tree)的知识,线段树是一种用于高效处理区间查询和更新操作的数据结构。它可以将一个数组或序列表示为一棵二叉树,每个节点代表数组中的一个区间,并存储该区间的一些统计信息。
线段树的构建过程是一个递归的过程,通过不断划分数组直至单个元素来构建线段树,其中每个节点都保存其所代表的区间的一些统计信息,如最大值、最小值、和、平均值等。
当谈到线段树时,我们还需要讨论一些相关的概念和技巧。首先,线段树通常用于解决一些特定类型的问题,例如区间最值查询、区间和查询等。通过巧妙地设计线段树节点所存储的信息,我们可以有效地解决这些问题。
在实际应用中,有时候我们还会遇到需要离散化处理数据的情况。离散化是将原始数据映射到连续的整数空间,这样可以减小数据规模,使得线段树的建立和查询更为高效。
此外,并非所有问题都适合使用线段树来解决,有时候其他数据结构如树状数组、平衡树等可能会更加适用。因此,我们需要对不同的数据结构有一定的了解,以便在解决具体问题时能够选择最合适的数据结构。
另外,还有一些线段树的优化技巧,例如懒惰标记(lazy propagation),可以在一定程度上提高线段树的更新效率,特别是在处理大规模数据时更为重要。
总的来说,线段树作为一种强大的数据结构,不仅需要掌握其基本原理和构建方法,还需要结合具体问题进行灵活运用,并且需要不断学习相关的优化技巧和应用场景,以便更好地解决实际问题。