一、引言
在算法竞赛和实际的开发过程中,我们经常会遇到需要对数组区间进行操作的问题,例如区间求和、区间求最值、区间更新等。如果使用朴素的方法来处理这些问题,时间复杂度往往较高,无法满足大规模数据的处理需求。而线段树(Segment Tree)作为一种二叉树形数据结构,能够高效地解决这些区间问题,将查询和更新操作的时间复杂度优化到 O(log n)。
二、线段树的基本概念
线段树是一种二叉树,每个节点代表一个区间。根节点代表整个数组区间,每个内部节点将其代表的区间一分为二,分别由左右子节点表示。叶子节点则代表数组中的单个元素。
例如,对于数组 [1, 3, 5, 7, 9, 11]
,其对应的线段树结构如下:
每个节点中的 [start, end]
表示该节点所代表的区间。
三、线段树节点定义
思路讲解
线段树是一种二叉树结构,每个节点都需要存储一些关键信息来表示其对应的区间和该区间的统计值。在这个问题中,我们主要处理区间和的问题,因此节点需要记录以下信息:
- 区间范围:通过
start
和end
来确定该节点所代表的数组区间。 - 区间和:使用
sum
存储该区间内所有元素的和。 - 懒标记:
lazy
用于区间更新操作,它可以延迟对子节点的更新,提高区间更新的效率。 - 子节点指针:
left
和right
分别指向该节点的左右子节点,以便构建树的结构。
代码展示
#include<bits/stdc++.h>
using namespace std;
// 线段树节点结构
struct SegmentTreeNode {
// 该节点所代表区间的起始位置
int start;
// 该节点所代表区间的结束位置
int end;
// 该节点所代表区间内元素的和
int sum;
// 懒标记,用于区间更新时延迟操作
int lazy;
// 指向左子节点的指针
SegmentTreeNode *left;
// 指向右子节点的指针
SegmentTreeNode *right;
// 构造函数,用于初始化节点信息
SegmentTreeNode(int s, int e) : start(s), end(e), sum(0), lazy(0), left(nullptr), right(nullptr) {}
};
四、 建树
思路讲解
建树的过程是一个递归的过程,从根节点开始,将整个数组区间逐步划分成更小的区间,直到每个区间只包含一个元素(即叶子节点)。具体步骤如下:
- 边界条件:如果当前区间的起始位置大于结束位置,说明该区间不存在,返回空指针。
- 创建节点:创建一个新的节点来代表当前区间。
- 叶子节点处理:如果当前区间只包含一个元素,将该元素的值赋给节点的
sum
。 - 递归构建子树:将当前区间一分为二,分别递归构建左子树和右子树。
- 更新节点和:当前节点的
sum
等于左子节点的sum
加上右子节点的sum
。
代码展示
// 构建线段树
// nums 为原始数组,start 为当前构建区间的起始位置,end 为当前构建区间的结束位置
SegmentTreeNode* buildTree(vector<int>& nums, int start, int end) {
// 如果起始位置大于结束位置,说明该区间不存在,返回空指针
if (start > end) return nullptr;
// 创建一个新的线段树节点,代表当前区间
SegmentTreeNode* root = new SegmentTreeNode(start, end);
// 如果起始位置等于结束位置,说明该区间只有一个元素,将该元素的值赋给节点的 sum
if (start == end) {
root->sum = nums[start];
return root;
}
// 计算区间的中间位置
int mid = start + (end - start) / 2;
// 递归构建左子树,左子树代表区间 [start, mid]
root->left = buildTree(nums, start, mid);
// 递归构建右子树,右子树代表区间 [mid + 1, end]
root->right = buildTree(nums, mid + 1, end);
// 当前节点的 sum 等于左子节点的 sum 加上右子节点的 sum
root->sum = root->left->sum + root->right->sum;
return root;
}
五、区间查询
思路讲解
区间查询的目的是获取指定区间内所有元素的和。在查询过程中,我们需要处理懒标记,以确保查询结果的准确性。具体步骤如下:
- 命中区间:如果当前节点代表的区间正好等于要查询的区间,直接返回该节点的
sum
。 - 下推懒标记:在递归查询子节点之前,先将当前节点的懒标记下推到子节点,更新子节点的
sum
和lazy
,并清空当前节点的lazy
。 - 递归查询:根据要查询的区间与当前节点代表区间的位置关系,递归查询左子树、右子树或同时查询左右子树。
代码展示
// 下推懒标记
void pushDown(SegmentTreeNode* root) {
if (root->lazy != 0) {
// 计算左右子树的区间长度
int leftLen = root->left->end - root->left->start + 1;
int rightLen = root->right->end - root->right->start + 1;
// 更新左子树的和以及懒标记
root->left->sum += leftLen * root->lazy;
root->left->lazy += root->lazy;
// 更新右子树的和以及懒标记
root->right->sum += rightLen * root->lazy;
root->right->lazy += root->lazy;
// 清空当前节点的懒标记
root->lazy = 0;
}
}
// 查询线段树中某个区间的和
// root 为线段树的根节点,start 为要查询区间的起始位置,end 为要查询区间的结束位置
int query(SegmentTreeNode* root, int start, int end) {
// 如果当前节点代表的区间正好等于要查询的区间,直接返回该节点的 sum
if (root->start == start && root->end == end) {
return root->sum;
}
// 下推懒标记
pushDown(root);
// 计算当前节点代表区间的中间位置
int mid = root->start + (root->end - root->start) / 2;
// 如果要查询的区间完全在左子树中,递归查询左子树
if (end <= mid) {
return query(root->left, start, end);
}
// 如果要查询的区间完全在右子树中,递归查询右子树
else if (start > mid) {
return query(root->right, start, end);
}
// 否则,要查询的区间跨越了左右子树,分别查询左右子树并将结果相加
else {
return query(root->left, start, mid) + query(root->right, mid + 1, end);
}
}
六、 单点更新
思路讲解
单点更新是指更新数组中某个特定位置的元素的值。在更新过程中,我们需要从根节点开始,递归地找到包含该元素的叶子节点,更新其 sum
,然后再更新其父节点的 sum
。具体步骤如下:
- 找到叶子节点:如果当前节点代表的区间只有一个元素,且该元素的索引等于要更新的索引,更新该节点的
sum
。 - 递归查找:根据要更新的索引与当前节点代表区间的中间位置的关系,递归地查找左子树或右子树。
- 更新父节点:更新完叶子节点后,回溯更新其父节点的
sum
。
代码展示
// 更新线段树中的某个节点
// root 为线段树的根节点,idx 为要更新的元素的索引,val 为新的值
void updateSingle(SegmentTreeNode* root, int idx, int val) {
// 如果当前节点代表的区间只有一个元素,且该元素的索引等于 idx,则更新该节点的 sum
if (root->start == root->end) {
root->sum = val;
return;
}
// 计算当前节点代表区间的中间位置
int mid = root->start + (root->end - root->start) / 2;
// 如果 idx 小于等于中间位置,说明要更新的元素在左子树中,递归更新左子树
if (idx <= mid) {
updateSingle(root->left, idx, val);
}
// 否则,说明要更新的元素在右子树中,递归更新右子树
else {
updateSingle(root->right, idx, val);
}
// 更新当前节点的 sum,等于左子节点的 sum 加上右子节点的 sum
root->sum = root->left->sum + root->right->sum;
}
七、区间更新
思路讲解
区间更新是指对数组中某个区间内的所有元素进行相同的操作(如加上一个固定的值)。为了提高效率,我们使用懒标记来延迟对子节点的更新。具体步骤如下:
- 完全包含区间:如果当前节点代表的区间完全在要更新的区间内,更新该节点的
sum
和lazy
。 - 下推懒标记:在递归更新子节点之前,先将当前节点的懒标记下推到子节点。
- 递归更新:根据要更新的区间与当前节点代表区间的位置关系,递归更新左子树、右子树或同时更新左右子树。
- 更新父节点:更新完子节点后,更新当前节点的
sum
。
代码展示
// 区间更新
// root 为线段树的根节点,start 为要更新区间的起始位置,end 为要更新区间的结束位置,val 为要增加的值
void updateRange(SegmentTreeNode* root, int start, int end, int val) {
// 如果当前节点代表的区间完全在要更新的区间内
if (root->start >= start && root->end <= end) {
// 计算当前区间的长度
int len = root->end - root->start + 1;
// 更新当前节点的和
root->sum += len * val;
// 标记当前节点需要更新子节点
root->lazy += val;
return;
}
// 下推懒标记
pushDown(root);
// 计算当前节点代表区间的中间位置
int mid = root->start + (root->end - root->start) / 2;
// 如果要更新的区间与左子树有交集,递归更新左子树
if (start <= mid) {
updateRange(root->left, start, end, val);
}
// 如果要更新的区间与右子树有交集,递归更新右子树
if (end > mid) {
updateRange(root->right, start, end, val);
}
// 更新当前节点的和
root->sum = root->left->sum + root->right->sum;
}
八、完整测试代码示例
#include <bits/stdc++.h>
using namespace std;
// 线段树节点结构
struct SegmentTreeNode {
// 该节点所代表区间的起始位置
int start;
// 该节点所代表区间的结束位置
int end;
// 该节点所代表区间内元素的和
int sum;
// 懒标记,用于区间更新时延迟操作
int lazy;
// 指向左子节点的指针
SegmentTreeNode *left;
// 指向右子节点的指针
SegmentTreeNode *right;
// 构造函数,用于初始化节点信息
SegmentTreeNode(int s, int e) : start(s), end(e), sum(0), lazy(0), left(nullptr), right(nullptr) {}
};
// 构建线段树
// nums 为原始数组,start 为当前构建区间的起始位置,end 为当前构建区间的结束位置
SegmentTreeNode* buildTree(vector<int>& nums, int start, int end) {
// 如果起始位置大于结束位置,说明该区间不存在,返回空指针
if (start > end) return nullptr;
// 创建一个新的线段树节点,代表当前区间
SegmentTreeNode* root = new SegmentTreeNode(start, end);
// 如果起始位置等于结束位置,说明该区间只有一个元素,将该元素的值赋给节点的 sum
if (start == end) {
root->sum = nums[start];
return root;
}
// 计算区间的中间位置
int mid = start + (end - start) / 2;
// 递归构建左子树,左子树代表区间 [start, mid]
root->left = buildTree(nums, start, mid);
// 递归构建右子树,右子树代表区间 [mid + 1, end]
root->right = buildTree(nums, mid + 1, end);
// 当前节点的 sum 等于左子节点的 sum 加上右子节点的 sum
root->sum = root->left->sum + root->right->sum;
return root;
}
// 下推懒标记
void pushDown(SegmentTreeNode* root) {
if (root->lazy != 0) {
// 计算左右子树的区间长度
int leftLen = root->left->end - root->left->start + 1;
int rightLen = root->right->end - root->right->start + 1;
// 更新左子树的和以及懒标记
root->left->sum += leftLen * root->lazy;
root->left->lazy += root->lazy;
// 更新右子树的和以及懒标记
root->right->sum += rightLen * root->lazy;
root->right->lazy += root->lazy;
// 清空当前节点的懒标记
root->lazy = 0;
}
}
// 查询线段树中某个区间的和
// root 为线段树的根节点,start 为要查询区间的起始位置,end 为要查询区间的结束位置
int query(SegmentTreeNode* root, int start, int end) {
// 如果当前节点代表的区间正好等于要查询的区间,直接返回该节点的 sum
if (root->start == start && root->end == end) {
return root->sum;
}
// 下推懒标记
pushDown(root);
// 计算当前节点代表区间的中间位置
int mid = root->start + (root->end - root->start) / 2;
// 如果要查询的区间完全在左子树中,递归查询左子树
if (end <= mid) {
return query(root->left, start, end);
}
// 如果要查询的区间完全在右子树中,递归查询右子树
else if (start > mid) {
return query(root->right, start, end);
}
// 否则,要查询的区间跨越了左右子树,分别查询左右子树并将结果相加
else {
return query(root->left, start, mid) + query(root->right, mid + 1, end);
}
}
// 更新线段树中的某个节点
// root 为线段树的根节点,idx 为要更新的元素的索引,val 为新的值
void updateSingle(SegmentTreeNode* root, int idx, int val) {
// 如果当前节点代表的区间只有一个元素,且该元素的索引等于 idx,则更新该节点的 sum
if (root->start == root->end) {
root->sum = val;
return;
}
// 计算当前节点代表区间的中间位置
int mid = root->start + (root->end - root->start) / 2;
// 如果 idx 小于等于中间位置,说明要更新的元素在左子树中,递归更新左子树
if (idx <= mid) {
updateSingle(root->left, idx, val);
}
// 否则,说明要更新的元素在右子树中,递归更新右子树
else {
updateSingle(root->right, idx, val);
}
// 更新当前节点的 sum,等于左子节点的 sum 加上右子节点的 sum
root->sum = root->left->sum + root->right->sum;
}
// 区间更新
// root 为线段树的根节点,start 为要更新区间的起始位置,end 为要更新区间的结束位置,val 为要增加的值
void updateRange(SegmentTreeNode* root, int start, int end, int val) {
// 如果当前节点代表的区间完全在要更新的区间内
if (root->start >= start && root->end <= end) {
// 计算当前区间的长度
int len = root->end - root->start + 1;
// 更新当前节点的和
root->sum += len * val;
// 标记当前节点需要更新子节点
root->lazy += val;
return;
}
// 下推懒标记
pushDown(root);
// 计算当前节点代表区间的中间位置
int mid = root->start + (root->end - root->start) / 2;
// 如果要更新的区间与左子树有交集,递归更新左子树
if (start <= mid) {
updateRange(root->left, start, end, val);
}
// 如果要更新的区间与右子树有交集,递归更新右子树
if (end > mid) {
updateRange(root->right, start, end, val);
}
// 更新当前节点的和
root->sum = root->left->sum + root->right->sum;
}
int main() {
// 定义原始数组
vector<int> nums = {1, 3, 5, 7, 9, 11};
// 构建线段树
SegmentTreeNode* root = buildTree(nums, 0, nums.size() - 1);
// 区间查询测试
int startQuery = 1, endQuery = 3;
int sumBefore = query(root, startQuery, endQuery);
cout << "Sum of elements in range [" << startQuery << ", " << endQuery << "] before any update: " << sumBefore << endl;
// 单点更新测试
int updateIndex = 2, updateValueSingle = 6;
updateSingle(root, updateIndex, updateValueSingle);
int sumAfterSingle = query(root, startQuery, endQuery);
cout << "Sum of elements in range [" << startQuery << ", " << endQuery << "] after single update: " << sumAfterSingle << endl;
// 区间更新测试
int startRangeUpdate = 1, endRangeUpdate = 3, updateValueRange = 2;
updateRange(root, startRangeUpdate, endRangeUpdate, updateValueRange);
int sumAfterRange = query(root, startQuery, endQuery);
cout << "Sum of elements in range [" << startQuery << ", " << endQuery << "] after range update: " << sumAfterRange << endl;
return 0;
}
九、真题训练
POJ 3468. A Simple Problem with Integers
解题思路
本题可使用线段树结合懒标记(Lazy Propagation)的方法来解决。以下是具体思路:
- 线段树节点定义:每个节点存储其所代表区间的起始位置、结束位置、区间内元素总和以及懒标记。懒标记用于在区间更新时,延迟对子节点的实际更新操作,提高效率。
- 建树操作:从根节点开始,递归地将整个数组区间不断二分,构建线段树。当区间只有一个元素时,该节点的区间和就是这个元素的值;否则,节点的区间和是其左右子节点区间和之和。
- 区间更新:当要更新的区间完全包含当前节点代表的区间时,更新当前节点的区间和,并给当前节点打上懒标记。若不完全包含,则先将当前节点的懒标记下推到子节点(更新子节点的区间和并传递懒标记),然后根据要更新区间与子区间的位置关系,递归地更新左右子树,最后更新当前节点的区间和(等于左右子节点区间和之和)。
- 区间查询:当查询区间完全包含当前节点代表的区间时,直接返回当前节点的区间和。若不完全包含,先将当前节点的懒标记下推到子节点,然后根据查询区间与子区间的位置关系,递归查询左右子树,将相关子树的查询结果累加起来得到最终查询结果。
通过上述线段树及相关操作的实现,能够高效地完成题目要求的区间更新和区间查询操作。
代码展示
#include <bits/stdc++.h>
using namespace std;
// 线段树节点结构
struct SegmentTreeNode {
int start, end; // 节点所代表区间的起始和结束位置
long long sum; // 该区间内所有元素的和
long long lazy; // 懒标记,用于区间更新时延迟操作
SegmentTreeNode *left, *right;
SegmentTreeNode(int s, int e) : start(s), end(e), sum(0), lazy(0), left(nullptr), right(nullptr) {}
};
// 构建线段树
SegmentTreeNode* buildTree(vector<int>& nums, int start, int end) {
if (start > end) return nullptr;
SegmentTreeNode* root = new SegmentTreeNode(start, end);
if (start == end) {
root->sum = nums[start]; // 叶子节点,区间只有一个元素,其和即为该元素的值
return root;
}
int mid = start + (end - start) / 2;
root->left = buildTree(nums, start, mid); // 递归构建左子树
root->right = buildTree(nums, mid + 1, end); // 递归构建右子树
root->sum = root->left->sum + root->right->sum; // 当前节点的和等于左右子节点的和
return root;
}
// 下推懒标记
void pushDown(SegmentTreeNode* root) {
if (root->lazy != 0) {
int leftLen = root->left->end - root->left->start + 1;
int rightLen = root->right->end - root->right->start + 1;
root->left->sum += (long long)leftLen * root->lazy; // 更新左子树的和
root->left->lazy += root->lazy; // 传递懒标记到左子树
root->right->sum += (long long)rightLen * root->lazy; // 更新右子树的和
root->right->lazy += root->lazy; // 传递懒标记到右子树
root->lazy = 0; // 清空当前节点的懒标记
}
}
// 区间更新操作
void updateRange(SegmentTreeNode* root, int start, int end, int val) {
if (root->start >= start && root->end <= end) {
int len = root->end - root->start + 1;
root->sum += (long long)len * val; // 更新当前节点的和
root->lazy += val; // 标记当前节点需要更新子节点
return;
}
pushDown(root); // 下推懒标记
int mid = root->start + (root->end - root->start) / 2;
if (start <= mid) updateRange(root->left, start, end, val); // 若更新区间与左子树有交集,递归更新左子树
if (end > mid) updateRange(root->right, start, end, val); // 若更新区间与右子树有交集,递归更新右子树
root->sum = root->left->sum + root->right->sum; // 更新当前节点的和
}
// 区间查询操作
long long query(SegmentTreeNode* root, int start, int end) {
if (root->start >= start && root->end <= end) {
return root->sum; // 当前节点完全包含在查询区间内,直接返回其和
}
pushDown(root); // 下推懒标记
int mid = root->start + (root->end - root->start) / 2;
long long result = 0;
if (start <= mid) result += query(root->left, start, end); // 若查询区间与左子树有交集,递归查询左子树
if (end > mid) result += query(root->right, start, end); // 若查询区间与右子树有交集,递归查询右子树
return result;
}
int main() {
int n, q;
cin >> n >> q; // 输入序列长度 n 和操作次数 q
vector<int> nums(n);
for (int i = 0; i < n; ++i) {
cin >> nums[i]; // 输入整数序列
}
SegmentTreeNode* root = buildTree(nums, 0, n - 1); // 构建线段树
for (int i = 0; i < q; ++i) {
char op;
cin >> op;
if (op == 'C') {
int l, r, d;
cin >> l >> r >> d;
--l; --r; // 转换为 0 索引
updateRange(root, l, r, d); // 执行区间更新操作
} else if (op == 'Q') {
int l, r;
cin >> l >> r;
--l; --r; // 转换为 0 索引
cout << query(root, l, r) << endl; // 执行区间查询操作并输出结果
}
}
return 0;
}
十、总结
本文围绕线段树展开,全面介绍了其原理、实现及应用:
- 原理与结构:线段树是一种二叉树结构,每个节点代表一个区间,通过对区间的不断划分构建树状结构。节点通常存储区间的起始和结束位置、区间内元素的统计值(如和)以及用于区间更新的懒标记等信息。
- 核心操作
- 建树:以递归方式从根节点开始,将原始数组区间逐步细分,直至每个叶子节点对应数组中的单个元素,同时计算并保存各节点的区间统计值。
- 区间查询:在查询指定区间时,根据当前节点与查询区间的位置关系,或直接返回节点统计值,或下推懒标记后递归查询子树,最终得到查询区间的统计结果。
- 单点更新:定位到需要更新的叶子节点,修改其值后,自底向上更新父节点的统计值,以维护线段树的正确性。
- 区间更新:利用懒标记机制,当更新区间包含当前节点区间时,标记懒标记并更新节点统计值;否则先下推懒标记,再递归更新子树,减少不必要的重复计算。
- 应用实例:以 POJ 3468 题目为例,展示了线段树在处理区间加值和区间求和问题上的应用。通过构建线段树,结合上述操作,高效解决了题目中对整数序列的动态区间操作需求。
总体而言,线段树是处理区间相关问题的有力工具,通过合理利用其结构和操作特性,能有效降低时间复杂度,提升算法效率,在算法竞赛和实际编程中都有广泛应用。