```cpp
class segment_tree
{
// 1. 核心结构与变量定义
// 通常用一个数组来模拟这棵树。如果一个父节点的数组下标是 p,那么它的左子节点下标是 2*p,右子节点是 2*p+1。这要求我们从下标 1 开始存储。
// 因此,如果原始数组的大小是 N,为了安全地存储整棵树,我们通常会开一个大小为 4*N 的数组。
// tree[p]:存储节点 p 所代表的区间的总和。
// lazy[p]:存储节点 p 的懒惰标记。标记的含义是“这个区间的所有元素都需要加上 lazy[p] 这个值,但我们暂时还没有把它传递给子节点”。
// a: 原始输入数组
// tree: 线段树数组,存储每个区间的和
// lazy: 懒惰标记数组
private:
long long a[100005];
long long tree[100005 * 4];
long long lazy[100005 * 4];
int n; // 原始数组的元素个数
public:
// 2. push_up (信息上传)
// 这个函数非常简单,它的作用是用子节点的信息更新父节点。对于区间求和来说,父节点的和就等于它两个子节点的和。
// 这个操作在建树和更新的递归回溯过程中会被频繁调用。
// 功能:用左右子节点的值更新父节点
// p: 当前节点在 tree 数组中的索引
void push_up(int p)
{
tree[p] = tree[p * 2] + tree[p * 2 + 1];
}
// 3. build (建树)
// 建树是一个递归的过程,将原始数组 a 的信息构建到 tree 数组中。
// 递归终点:当区间 [l, r] 的 l == r 时,说明到达了叶子节点,它代表原始数组中的单个元素。
// 递归过程:
// 找到当前区间 [l, r] 的中点 mid。
// 递归地为左子区间 [l, mid] 和右子区间 [mid+1, r] 建树。
// 建完子树后,用 push_up 函数更新当前节点的值。
// 功能:构建线段树
// p: 当前节点在 tree 数组中的索引
// l, r: 当前节点所代表的区间 [l, r]
// 调用入口: build(1, 1, n);
void build(int p, int l, int r)
{
if (l == r)
{
// 到达叶子节点,直接赋值
tree[p] = a[l];
return;
}
int mid = l + (r - l) / 2; // 计算中点,防止整数溢出
build(p * 2, l, mid); // 递归构建左子树
build(p * 2 + 1, mid + 1, r); // 递归构建右子树
push_up(p); // 用子节点信息更新当前节点
}
// 4. push_down (懒惰标记下传)
// 这是线段树的精髓。当我们需要更新或查询一个经过带有懒惰标记的节点的子节点时,我们必须先将这个标记传递下去。
// 功能:将父节点 p 的懒惰标记 lazy[p] 应用到它的两个子节点上。
// 步骤:
// 检查当前节点 p 是否有懒惰标记。如果没有,直接返回。
// 更新左子节点 p*2 的 tree 值和 lazy 标记。tree 值增加的大小是 lazy[p] 乘以左子区间的长度。
// 更新右子节点 p*2+1 的 tree 值和 lazy 标记。
// 清除当前节点 p 的懒惰标记(因为它已经成功“下传”了)。
// 功能:下传懒惰标记
// p: 当前节点在 tree 数组中的索引
// l, r: 当前节点所代表的区间 [l, r]
void push_down(int p, int l, int r)
{
if (lazy[p] != 0)
{ // 如果有标记
int mid = l + (r - l) / 2;
int len_left = mid - l + 1; // 左子区间的长度
int len_right = r - mid; // 右子区间的长度
// 更新左子节点的 tree 值和 lazy 标记
tree[p * 2] += lazy[p] * len_left;
lazy[p * 2] += lazy[p];
// 更新右子节点的 tree 值和 lazy 标记
tree[p * 2 + 1] += lazy[p] * len_right;
lazy[p * 2 + 1] += lazy[p];
// 清除当前节点的标记
lazy[p] = 0;
}
}
// 5. update (区间修改)
// 现在我们可以实现区间修改了,比如将 [update_l, update_r] 区间内的每个数都加上 val。
// 递归逻辑:
// 完全包含:如果当前节点代表的区间 [l, r] 被要修改的区间 [update_l, update_r] 完全包含,我们直接更新当前节点的 tree 值,并给它打上懒惰标记 lazy,然后返回。我们不需要再往下递归,这就是“懒惰”的体现。
// 部分相交:如果当前区间与目标区间有交集,但不是完全包含:
// 先 push_down,确保子节点的状态是最新的。
// 递归地到可能相交的左子节点或右子节点去执行 update。
// 子节点更新完毕后,push_up 更新当前节点。
// 调用入口: update(1, 1, n, start, end, value);
// 功能:区间修改,将 [update_l, update_r] 内的每个数加上 val
void update(int p, int l, int r, int update_l, int update_r, long long val)
{
// 情况 2: 完全不相交
// 如果当前区间与修改区间无交集,直接返回,什么都不做。
if (r < update_l || l > update_r)
{
return;
}
// 情况 1: 完全包含
// 如果当前区间被修改区间完全包含,则更新当前节点并打上懒惰标记。
if (update_l <= l && r <= update_r)
{
tree[p] += val * (r - l + 1);
lazy[p] += val;
return;
}
// 情况 3: 部分相交
// 先下传懒惰标记,然后递归到子节点进行更新。
push_down(p, l, r);
int mid = l + (r - l) / 2;
update(p * 2, l, mid, update_l, update_r, val);
update(p * 2 + 1, mid + 1, r, update_l, update_r, val);
// 不要忘记用更新后的子节点信息来更新当前节点。
push_up(p);
}
// 6. query (区间查询)
// 查询与更新的逻辑非常相似。
// 递归逻辑:
// 完全包含:如果当前节点代表的区间 [l, r] 被查询区间 [query_l, query_r] 完全包含,直接返回当前节点的 tree 值。
// 部分相交:
// 先 push_down,确保子节点的值是最新的。
// 递归地到相交的左、右子节点去查询,并将结果累加起来。
// 完全不相交:返回一个不影响结果的“单位元”(对于求和是 0,求最大值是 -∞)。
// 调用入口: query(1, 1, n, start, end);
// 功能:区间查询,返回 [query_l, query_r] 的和
long long query(int p, int l, int r, int query_l, int query_r)
{
// 情况 2: 完全不相交
// 如果当前区间 [l, r] 与查询区间 [query_l, query_r] 没有任何交集,
// 那么当前子树对结果没有任何贡献,返回单位元 0。
if (r < query_l || l > query_r)
{
return 0; // 对于求和问题,单位元是 0
}
// 情况 1: 完全包含
// 如果当前区间被查询区间完全包含,直接返回当前节点的值。
if (query_l <= l && r <= query_r)
{
return tree[p];
}
// 情况 3: 部分相交
// 必须先下传懒惰标记,确保子节点的信息是正确的。
push_down(p, l, r);
int mid = l + (r - l) / 2;
long long sum = 0;
// 递归查询左右子节点,并将结果累加。
// 注意:这里不再需要 if 判断,因为上面“完全不相交”的检查会处理好一切。
// 如果查询区间只与一个子区间相交,另一个子区间的递归调用会因为不相交而立刻返回 0。
sum += query(p * 2, l, mid, query_l, query_r);
sum += query(p * 2 + 1, mid + 1, r, query_l, query_r);
return sum;
}
};