树状数组维护前缀最值
在单调修改条件下,用树状数组支持单点 chmax 与前缀最大值查询。
“选学分支”
本篇只依赖基础树状数组,不属于“差分 → 双树状数组”的求和主线。
前置回顾
基础树状数组节点 tree[i] 管辖区间:
前缀查询通过 i -= lowbit(i) 把 i += lowbit(i) 枚举所有覆盖修改位置的块。
本篇保留这套块结构,只把块内运算从“求和”换成“取最大值”。
一句话算法
当一个位置只会变大时,把它沿树状数组更新路径取 max,查询前缀时再合并各块最大值。
问题模型
本篇以最大值为例,严格支持以下操作:
- 单调单点修改:
- 前缀查询:求
两种操作都要求在线完成。
“适用边界”
这里的修改是 chmax,不是任意赋值。若把某个位置从大值改小,旧最大值可能仍残留在多个树状数组节点中,简单更新无法删除它。
核心直觉
令每个节点保存自己管辖区间的最大值:
基础求和树状数组依赖两个性质:
- 一个点只影响
个覆盖它的块; - 一个前缀可以拆成
个互不重叠的块。
取最大值同样可以合并这些块:
而 chmax 修改不会让任何已有最大值失效。一个块的新最大值只可能是:
所以修改路径上直接取 max 即可。
算法步骤
初始化
树状数组节点初始化为负无穷。依次把初始数组加入:
1
2
3
for (int i = 1; i <= n; ++i) {
bit.chmax(i, a[i]);
}
单点 chmax
对 a[pos] = max(a[pos], value):
1
2
3
for (int i = pos; i <= n; i += lowbit(i)) {
tree[i] = max(tree[i], value);
}
这些节点正是所有覆盖 pos 的块。
查询前缀最大值
1
2
3
4
answer = negative_infinity;
for (int i = right; i > 0; i -= lowbit(i)) {
answer = max(answer, tree[i]);
}
例如查询
[1,7] = [7,7] + [5,6] + [1,4]
answer = max(tree[7], tree[6], tree[4])
小例子
初始数组为
prefix_max(4) = 4
chmax(2, 6) -> 数组变为 [2,6,4,3,5]
prefix_max(3) = 6
chmax(5, 1) -> 位置 5 不会变小
prefix_max(5) = 6
算法证明
核心不变量:tree[i] 始终等于其管辖区间的最大值。
- 初始化时,每个
都沿更新路径加入所有覆盖它的块,因此每个块收集到区间内全部元素的最大值。 - 执行
chmax(pos, value)时,只有覆盖pos的块可能变大。对这些块取max(tree[i], value),恰好得到修改后的块最大值。 - 前缀查询选出的块两两不交并刚好覆盖
。对这些块的最大值再次取最大值,就得到整个前缀的最大值。
因此,在单调修改条件下,修改和查询都正确。
复杂度分析
- 初始化:
; - 单点
chmax:; - 前缀最大值查询:
; - 空间复杂度:
。
代码模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <bits/stdc++.h>
using namespace std;
// 单点 chmax、前缀最大值。
// 更新只能写成 a[pos] = max(a[pos], value),不能任意赋值。
template <typename T>
struct FenwickPrefixMax {
int n = 0;
T identity = numeric_limits<T>::lowest();
vector<T> tree;
FenwickPrefixMax(int n = 0) { init(n); }
void init(int size) {
n = size;
tree.assign(n + 1, identity);
}
static int lowbit(int x) { return x & -x; }
void chmax(int pos, T value) {
for (int i = pos; i <= n; i += lowbit(i)) {
tree[i] = max(tree[i], value);
}
}
T prefix_max(int pos) const {
T answer = identity;
for (int i = pos; i > 0; i -= lowbit(i)) {
answer = max(answer, tree[i]);
}
return answer;
}
};
如果维护前缀最小值,把负无穷换成正无穷,并把所有 max 换成 min;对应修改必须是 chmin。
完整代码
本篇使用下面的自拟模板任务:
1 x v:执行a[x] = max(a[x], v);2 r:查询的最大值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <bits/stdc++.h>
using namespace std;
template <typename T>
struct FenwickPrefixMax {
int n = 0;
T identity = numeric_limits<T>::lowest();
vector<T> tree;
FenwickPrefixMax(int n = 0) { init(n); }
void init(int size) {
n = size;
tree.assign(n + 1, identity);
}
static int lowbit(int x) { return x & -x; }
void chmax(int pos, T value) {
for (int i = pos; i <= n; i += lowbit(i)) {
tree[i] = max(tree[i], value);
}
}
T prefix_max(int pos) const {
T answer = identity;
for (int i = pos; i > 0; i -= lowbit(i)) {
answer = max(answer, tree[i]);
}
return answer;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
FenwickPrefixMax<long long> bit(n);
for (int i = 1; i <= n; ++i) {
long long value;
cin >> value;
bit.chmax(i, value);
}
while (m--) {
int operation;
cin >> operation;
if (operation == 1) {
int pos;
long long value;
cin >> pos >> value;
bit.chmax(pos, value);
} else {
int right;
cin >> right;
cout << bit.prefix_max(right) << '\n';
}
}
return 0;
}
测试用例
输入:
5 5
2 1 4 3 5
2 4
1 2 6
2 3
1 5 1
2 5
输出:
4
6
6
易错点
- 任意赋值不满足单调条件,尤其不能用这份模板把较大值改小。
max(prefix(r))与max(prefix(l-1))不能相减,因此这份模板不能直接回答一般的最大值。 - 初始值可能为负数,节点必须初始化为负无穷,不能默认初始化为
。 - 一般静态区间最值优先使用 ST 表;支持任意单点修改时优先使用线段树。
主练习
实现本篇的自拟模板任务,并额外测试以下边界:
- 所有初始值均为负数;
chmax传入的值小于当前位置;- 查询
与 。
完成标准是:能够解释为什么 chmax 可以直接更新块最大值,而任意赋值不可以。
拓展题
luogu-P1198 题解
P1198 只在数组尾部追加元素,并查询最后若干项的最大值。它可以进一步利用树状数组块完成区间最大值查询,但需要保存原数组,并在完整块越过左端点时退化为单点处理。
这比本篇的“单调修改 + 前缀最大值”多一层区间拆分,因此放在完成主练习之后。