文章目录
线段树模板:区间和与区间更新
线段树是一种非常强大的数据结构,它允许我们在对数时间内对数组的区间执行查询和更新操作。在这个教程中,我们将通过一个具体的例子——使用线段树来解决洛谷P3372 【模板】线段树 1“区间求和”和“区间更新”的问题来学习线段树的基础模板的应用。
我并不打算动动手指就能把线段树的工作原理描述得非常细致易懂。
如果读者完全不明白线段树,建议在拥有二叉树
的前置知识下,对着这里的代码敲一遍,相信你会豁然开朗,自己敲一遍比看别人说一千次还要有效。
线段树的基本概念
线段树是一个二叉树,每个节点代表一个区间的聚合信息(例如区间和、最大值或最小值)。对于长度为n
的数组,线段树大约需要4 * n
的空间来存储(至于为什么是4 * n的空间,请看oi-wiki,这里不多赘述)。
构建线段树
构建线段树涉及将数组分解为多个区间,并将这些区间作为树的叶子节点。内部节点存储子区间的聚合值。构建树的过程是递归的,时间复杂度为O(n)
。
构建函数
inline void buildTree(int l, int r, int node_idx = 1) {
tree[node_idx].l = l;
tree[node_idx].r = r;
if (l == r) {
tree[node_idx].sum = nums[l];
return;
}
int double_idx = (node_idx << 1);
int left_chil_idx = double_idx + 1;
int right_chil_idx = double_idx + 2;
int mid = l + ((r - l) >> 1);
buildTree(l, mid, left_chil_idx);
buildTree(mid + 1, r, right_chil_idx);
tree[node_idx].sum = tree[left_chil_idx].sum + tree[right_chil_idx].sum;
}
在这个函数中,我们从根节点开始,递归地将数组分成两部分,直到每个区间只包含一个元素。我们将数组的元素存储在树的叶子节点,然后计算内部节点的值,它是其左右子节点值的和。
懒惰更新
down
函数,将之前保存在lazy
数组中没有真正进行更新操作的数字,真正执行一次更新操作。
在区间查询和区间更新操作中,如果访问到了某一个节点,并且还要继续深入访问其子节点,就应该将之前保存下来的懒惰更新的值进行真正的更新。
inline void down(int node_idx) {
if (lazy[node_idx] == 0) return;
int tree_left = tree[node_idx].l;
int tree_right = tree[node_idx].r;
int mid = tree_left + ((tree_right - tree_left) >> 1);
int double_idx = (node_idx << 1);
int left_chil_idx = double_idx + 1;
int right_chil_idx = double_idx + 2;
tree[left_chil_idx].sum += lazy[node_idx] * (mid - tree_left + 1);
tree[right_chil_idx].sum += lazy[node_idx] * (tree_right - mid);
lazy[left_chil_idx] += lazy[node_idx];
lazy[right_chil_idx] += lazy[node_idx];
lazy[node_idx] = 0;
}
区间查询
区间查询是线段树的核心功能之一。我们可以使用线段树在O(log n)
时间内查询任意区间的聚合信息。
查询函数
inline int queryRange(int query_left, int query_right, int node_idx = 1) {
int tree_left = tree[node_idx].l, tree_right = tree[node_idx].r;
// 完全不与查询目标的区间重合
if (tree_left > query_right || tree_right < query_left) return 0;
// 完全与查询目标的区间重合
if