区间修改与区间求和是算法竞赛中的经典题型,也是数据结构课程中的重要知识点。这道蓝桥杯算法模板题1133考察的是对一维数组进行高效区间操作的能力。在实际编程竞赛中,这类问题往往作为基础题出现,但若处理不当会成为性能瓶颈。
题目通常会给出一个长度为N的数组,要求实现两种操作:
暴力解法的时间复杂度为O(N)每次操作,当N较大(如1e5)时无法通过时间限制。因此需要采用更高效的数据结构实现O(logN)级别的操作。
线段树是解决此类问题的首选数据结构。它将原始数组构建为一棵二叉树,每个节点存储对应区间的统计信息(如区间和)。对于长度为N的数组,线段树的空间复杂度为O(N),构建时间复杂度为O(N)。
线段树的优势在于:
树状数组(Fenwick Tree)是另一种可选方案,通过巧妙的位运算实现高效的前缀和查询。对于单纯的区间求和问题,树状数组的代码更简洁,常数更小。
但原生树状数组只支持单点修改和前缀查询。要实现区间修改,需要引入差分思想,使用两个树状数组来维护。这使得实现复杂度增加,在竞赛中不如线段树直观。
cpp复制const int MAXN = 1e5+5;
struct Node {
int l, r; // 区间左右端点
long long sum; // 区间和
long long lazy; // 延迟标记
} tree[MAXN<<2];
cpp复制void build(int p, int l, int r) {
tree[p].l = l, tree[p].r = r;
tree[p].lazy = 0;
if(l == r) {
tree[p].sum = a[l];
return;
}
int mid = (l + r) >> 1;
build(p<<1, l, mid);
build(p<<1|1, mid+1, r);
tree[p].sum = tree[p<<1].sum + tree[p<<1|1].sum;
}
cpp复制void push_down(int p) {
if(tree[p].lazy) {
int lson = p<<1, rson = p<<1|1;
tree[lson].sum += tree[p].lazy * (tree[lson].r - tree[lson].l + 1);
tree[rson].sum += tree[p].lazy * (tree[rson].r - tree[rson].l + 1);
tree[lson].lazy += tree[p].lazy;
tree[rson].lazy += tree[p].lazy;
tree[p].lazy = 0;
}
}
cpp复制void update(int p, int l, int r, int val) {
if(l <= tree[p].l && tree[p].r <= r) {
tree[p].sum += val * (tree[p].r - tree[p].l + 1);
tree[p].lazy += val;
return;
}
push_down(p);
int mid = (tree[p].l + tree[p].r) >> 1;
if(l <= mid) update(p<<1, l, r, val);
if(r > mid) update(p<<1|1, l, r, val);
tree[p].sum = tree[p<<1].sum + tree[p<<1|1].sum;
}
cpp复制long long query(int p, int l, int r) {
if(l <= tree[p].l && tree[p].r <= r) {
return tree[p].sum;
}
push_down(p);
int mid = (tree[p].l + tree[p].r) >> 1;
long long res = 0;
if(l <= mid) res += query(p<<1, l, r);
if(r > mid) res += query(p<<1|1, l, r);
return res;
}
设原数组为a[],定义差分数组d[],其中d[i] = a[i] - a[i-1](d[1] = a[1])。这样区间[L,R]加V可以转化为:
前缀和sum[1..k] = Σd[1..i] = a[k]
cpp复制long long t1[MAXN], t2[MAXN]; // 两个树状数组
void add(int k, int v) {
int v1 = k * v;
for(; k <= n; k += k&-k) {
t1[k] += v, t2[k] += v1;
}
}
long long query(int k) {
long long res = 0;
for(int i = k; i; i -= i&-i) {
res += (k+1)*t1[i] - t2[i];
}
return res;
}
// 区间[L,R]加V
void range_add(int l, int r, int v) {
add(l, v), add(r+1, -v);
}
// 查询[L,R]和
long long range_query(int l, int r) {
return query(r) - query(l-1);
}
| 操作 \ 方案 | 线段树 | 树状数组 |
|---|---|---|
| 建树/初始化 | O(N) | O(NlogN) |
| 区间修改 | O(logN) | O(logN) |
| 区间查询 | O(logN) | O(logN) |
线段树需要4N空间,树状数组需要2N空间。对于内存限制严格的场景,树状数组更有优势。
建议设计以下测试用例:
线段树可以轻松扩展支持:
对于值域很大但实际数据稀疏的情况,可以使用动态开点线段树,避免预先分配大量空间。
通过二维线段树或树状数组,可以处理矩阵的区间操作问题,如子矩阵求和等。
在实际编码中,我发现以下几点特别重要:
调试时建议: