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

线段树

在计算机科学领域,处理区间相关的问题是常见的任务。无论是在算法竞赛中,还是在实际的应用开发中,如游戏排行榜系统、文本编辑器、系统监控、地理信息系统等,都需要高效地对区间进行查询和修改操作。线段树(Segment Tree)作为一种强大的数据结构,为这类问题提供了高效的解决方案。

Part-1. 线段树的概念

1.1 什么是线段树

线段树本质上是一种二叉搜索树,将区间划分为多个小区间,并用树状结构管理这些区间的数据结构。它是一棵二叉树,每个节点代表一个区间,节点存储这个区间的某种统计信息,例如区间和、区间最大值、区间最小值、区间内不同元素的个数等。
1534760b7d2def13e7d15418159c1bf7

1.2 为什么需要线段树

想象有一个长度为 n 的数组,我们经常需要进行以下两种操作:
区间查询:查询数组中某个区间内元素的某种统计信息,比如区间和、最大值、最小值等。
区间修改:对数组中某个区间内的所有元素进行相同的修改操作,如增加一个固定的值。
对于这类问题,如果直接使用数组进行操作,区间查询的时间复杂度可能会达到 O (n),而区间修改操作如果涉及大量元素,时间复杂度也会很高。而线段树通过巧妙的区间划分和信息聚合机制,能够在 O (log n) 的时间复杂度内完成区间查询和修改操作,大大提高了效率。

1.3 线段树的本质

线段树的本质是一种基于二分思想的数据结构。它将一个大区间递归地划分为更小的区间,直到区间长度为 1(即叶子节点)。每个父节点存储的信息是由其子节点的信息聚合而来,这种结构使得线段树在处理区间问题时具有高效性。

Part-2. 线段树的设计思想

2.1 核心思想
区间划分:将大区间递归地划分为更小的区间,直到不能再分(区间长度为 1)。例如,对于区间 [1, 8],首先会被划分为 [1, 4] 和 [5, 8],然后 [1, 4] 又会被划分为 [1, 2] 和 [3, 4],以此类推,直到每个子区间只包含一个元素,成为叶子节点。
信息聚合:父节点存储的信息是由其子节点的信息聚合而来。比如,如果我们要构建一个用于求区间和的线段树,那么父节点的区间和就是其左子节点区间和与右子节点区间和之和。
懒惰更新:当需要对一个大区间进行修改时,不立即修改所有子节点,而是先标记父节点,等到需要用到子节点时再更新。这一策略大大减少了不必要的节点更新操作,提高了区间修改的效率。

2.2 为什么这样设计是高效的

二分思想:在查询时,如果当前区间完全包含在目标区间内,直接返回结果,避免了对该区间子节点的不必要遍历;如果部分重叠,则递归处理子区间。由于每次递归都会将区间规模减半,平均只需要访问 O (log n) 个节点,大大提高了查询效率。
区间管理:每个节点管理一个连续区间,父节点区间等于左子节点区间加上右子节点区间。这种层次分明的区间管理方式使得区间操作变得高效,无论是查询还是修改,都能快速定位到相关的区间节点。

Part-3. 线段树的应用场景

算法题中的应用
区间统计类问题:例如查询区间内有多少个不同的数、查询区间内的众数等。线段树可以通过维护区间内的元素信息,高效地解决这类问题。
动态维护区间信息:比如维护区间最大连续子段和、维护区间 GCD(最大公因数)等。线段树的灵活性使得它能够在数据动态变化的情况下,依然保持高效的区间信息维护能力。

4. 线段树的优化技巧

4.1 内存优化

动态开点:适用场景为区间范围很大但实际数据稀疏。实现方式是用 map 或指针动态分配节点,而不是预先分配一个固定大小的数组来存储所有可能的节点。这样可以大大减少内存使用,避免因区间范围过大导致的内存浪费。
离散化:当数值范围很大但数据量较小的时候,离散化是一种有效的优化方法。它将原始值映射到更小的范围,减少了树的大小,从而提高效率。例如,原始数据中的值可能在 1 到 10^9 之间,但实际只有 100 个不同的值,通过离散化可以将这些值映射到 1 到 100 之间,大大减小了线段树的规模。

4.2 性能优化

启发式合并:在需要合并两个线段树的场景中适用。实现方式是总是将小树合并到大树上,这样可以减少合并操作的时间复杂度。因为将小树合并到大树上,对大树的结构影响相对较小,从而减少了重新构建和调整节点信息的开销。
位运算优化:在计算父子节点关系时使用位运算可以提高效率。例如,对于一个节点的编号 i,其左子节点的编号可以通过 (i << 1) + 1 计算得到,右子节点的编号可以通过 (i << 1) + 2 计算得到。在一些区间操作中,合理使用位运算也可以优化代码性能。

Part-5. 线段树的扩展

5.1 二维线段树

二维线段树用于处理矩形区域的查询和修改问题。它可以看作是对一维线段树的扩展,将一维区间扩展为二维矩形区域。在实现上,通常需要使用两棵线段树,一棵用于处理行方向的区间,另一棵用于处理列方向的区间。时间复杂度为 O (log n * log m),空间复杂度为 O (n * m),其中 n 和 m 分别是矩形区域在行和列方向上的大小。

5.2 可持久化线段树

可持久化线段树支持查询历史版本,每次修改只需要 O (log n) 的额外空间。它适用于需要维护历史信息的场景,例如在一些版本控制系统中,需要记录文件在不同时间点的状态,可持久化线段树可以高效地实现这一功能。

Part-6. 线段树的基本操作

6.1 构建线段树

线段树的构建通常采用递归方式。以下是一个用 Go 语言实现的构建支持区间求和的线段树的示例代码:

type SegmentTree struct {tree   []int64lazy   []int64n      intmerge  func(int64, int64) int64
}func NewSegmentTree(arr []int64, mergeFunc func(int64, int64) int64) *SegmentTree {n := len(arr)tree := make([]int64, 4*n)lazy := make([]int64, 4*n)st := &SegmentTree{tree:   tree,lazy:   lazy,n:      n,merge:  mergeFunc,}st.buildTree(1, 0, n-1, arr)return st
}func (st *SegmentTree) buildTree(node, start, end int, arr []int64) int64 {if start == end {st.tree[node] = arr[start]return st.tree[node]}mid := start + (end-start)/2leftNode := 2*node + 1rightNode := 2*node + 2leftSum := st.buildTree(leftNode, start, mid, arr)rightSum := st.buildTree(rightNode, mid+1, end, arr)st.tree[node] = st.merge(leftSum, rightSum)return st.tree[node]
}

在这段代码中,buildTree函数递归地构建线段树,node表示当前节点在tree数组中的索引,start和end表示当前节点表示的区间范围。如果是叶子节点(start == end),则直接将对应位置的数组元素值赋给当前节点;否则,先递归构建左右子树,然后通过merge函数将左右子树的结果聚合得到当前节点的值。

6.2 区间查询

区间查询操作在 O (log n) 的时间复杂度内完成。以下是区间查询的示例代码:

func (st *SegmentTree) queryRange(node, start, end, l, r int) int64 {if st.lazy[node] != 0 {st.tree[node] += st.lazy[node] * int64(end-start+1)if start != end {st.lazy[2*node+1] += st.lazy[node]st.lazy[2*node+2] += st.lazy[node]}st.lazy[node] = 0}if l <= start && r >= end {return st.tree[node]}if r < start || l > end {return 0}mid := start + (end-start)/2leftSum := int64(0)rightSum := int64(0)if l <= mid {leftSum = st.queryRange(2*node+1, start, mid, l, r)}if r > mid {rightSum = st.queryRange(2*node+2, mid+1, end, l, r)}return st.merge(leftSum, rightSum)
}

在queryRange函数中,首先检查当前节点的懒惰标记,如果有标记则进行下推更新。然后判断当前区间与目标查询区间的关系,如果当前区间完全包含在目标区间内,则直接返回当前节点的值;如果没有交集,则返回 0;如果部分重叠,则分别递归查询左右子树,并通过merge函数合并结果。

6.3 区间更新

区间更新操作同样利用懒惰标记来优化,避免对每个受影响的节点都进行即时更新。以下是区间更新的示例代码:

func (st *SegmentTree) updateRange(node, start, end, l, r int, val int64) {if st.lazy[node] != 0 {st.tree[node] += st.lazy[node] * int64(end-start+1)if start != end {st.lazy[2*node+1] += st.lazy[node]st.lazy[2*node+2] += st.lazy[node]}st.lazy[node] = 0}if l <= start && r >= end {st.tree[node] += val * int64(end-start+1)if start != end {st.lazy[2*node+1] += valst.lazy[2*node+2] += val}return}if r < start || l > end {return}mid := start + (end-start)/2st.updateRange(2*node+1, start, mid, l, r, val)st.updateRange(2*node+2, mid+1, end, l, r, val)st.tree[node] = st.merge(st.tree[2*node+1], st.tree[2*node+2])
}

在updateRange函数中,同样先处理当前节点的懒惰标记。如果当前区间完全包含在要更新的区间内,则更新当前节点的值,并给子节点打上懒惰标记;如果部分重叠,则递归更新左右子树,最后更新当前节点的值。

下面是一个比较通用的线段树模板,支持区间加法 + 区间乘法 + 区间求和:

using namespace std;
typedef long long ll;class SegmentTree {
public:struct Node {ll l, r;ll sum, add, mul;};vector<Node> tree;vector<ll> data;SegmentTree(const vector<ll>& input) {data = input;int size = input.size() - 1; // 1-basedtree.resize(size * 4 + 10);build(1, 1, size);}void build(int i, int l, int r) {tree[i] = {l, r, 0, 0, 1};if (l == r) {tree[i].sum = data[l];return;}int mid = (l + r) >> 1;build(i << 1, l, mid);build(i << 1 | 1, mid + 1, r);push_up(i);}void range_add(int l, int r, ll val) {add(1, l, r, val);}void range_mult(int l, int r, ll val) {mult(1, l, r, val);}ll range_query(int l, int r) {return query(1, l, r);}private:void push_up(int i) {tree[i].sum = tree[i << 1].sum + tree[i << 1 | 1].sum;}void push_down(int i) {ll mul = tree[i].mul, add = tree[i].add;apply(i << 1, mul, add);apply(i << 1 | 1, mul, add);tree[i].mul = 1;tree[i].add = 0;}void apply(int i, ll mul, ll add) {tree[i].sum = tree[i].sum * mul + (tree[i].r - tree[i].l + 1) * add;tree[i].mul *= mul;tree[i].add = tree[i].add * mul + add;}void add(int i, int l, int r, ll val) {if (tree[i].l >= l && tree[i].r <= r) {apply(i, 1, val);return;}push_down(i);if (tree[i << 1].r >= l) add(i << 1, l, r, val);if (tree[i << 1 | 1].l <= r) add(i << 1 | 1, l, r, val);push_up(i);}void mult(int i, int l, int r, ll val) {if (tree[i].l >= l && tree[i].r <= r) {apply(i, val, 0);return;}push_down(i);if (tree[i << 1].r >= l) mult(i << 1, l, r, val);if (tree[i << 1 | 1].l <= r) mult(i << 1 | 1, l, r, val);push_up(i);}ll query(int i, int l, int r) {if (tree[i].l >= l && tree[i].r <= r)return tree[i].sum;push_down(i);ll res = 0;if (tree[i << 1].r >= l) res += query(i << 1, l, r);if (tree[i << 1 | 1].l <= r) res += query(i << 1 | 1, l, r);return res;}
};
int main() {ll n, m;cin >> n >> m;vector<ll> arr(n + 1); // 1-based indexfor (int i = 1; i <= n; i++) {cin >> arr[i];}SegmentTree seg(arr);while (m--) {int op;cin >> op;if (op == 1) {ll l, r, val;cin >> l >> r >> val;seg.range_mult(l, r, val);} else if (op == 2) {ll l, r, val;cin >> l >> r >> val;seg.range_add(l, r, val);} else if (op == 3) {ll l, r;cin >> l >> r;cout << seg.range_query(l, r) << endl;}}return 0;
}

Part-7. 总结

线段树是一个强大的数据结构,特别适合处理区间查询和修改操作。虽然其实现相对复杂,但在需要频繁进行区间操作的场景中,它的效率远超普通的数组操作。通过合理运用线段树的优化技巧和扩展形式,能够进一步提升其在不同场景下的性能表现。掌握线段树的原理和实现,对解决区间相关问题具有很大的帮助,无论是在算法竞赛中,还是在实际的软件开发中,都能发挥重要作用。

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

相关文章:

  • 8/16
  • P5204 [USACO19JAN] Train Tracking 2 P
  • Pycharm选择使用miniconda创建的虚拟环境
  • 深入解析:【Nginx前端需要了解的基础配置】
  • C4NR服务器装备淬炼系统详细介绍
  • 电脑连接联想模拟器
  • Linux用户管理命令
  • c语言把两个对称矩阵存为一维数组,再求出两个对称矩阵的乘积
  • 使用KRaft部署单点kafka集群
  • LOJ6076 做题记录 - 邻补角
  • 自动布局
  • 浅谈拉格朗日插值
  • 5.3 绝对路径和相对路径
  • WrenAI部署,解决发送消息报错:failed to create asking task
  • ORM在企业级项目中的应用
  • 记录一个神秘做法
  • 单元测试三大神器:unittest vs JUnit vs Jest 终极对决
  • 数据点配置工具使用教程
  • 怎么马上上大学了
  • 深入解析:Java集合类综合练习题
  • 爬虫入门
  • 关键词提取实战指南:方法选择与应用注意事项解析
  • 软件测试基础知识 + 面试理论(超详细)
  • 在AI技术快速落地的时代,挖掘创意工具的新需求成为关键——某知名Adobe插件框架需求分析
  • day22
  • ESP32-S3 控制 BMP280气压传感器
  • OI 如何配置 Visual Studio Code
  • ESP32-S3 控制 传感器模块
  • ESP32-S3 控制 红外寻迹模块
  • ESP32-S3 控制 红外避障模块