树状数组维护前缀最值

代码模板

在单调修改条件下,用树状数组支持单点 chmax 与前缀最大值查询。

选学分支

本篇只依赖基础树状数组,不属于“差分 → 双树状数组”的求和主线。

前置回顾

基础树状数组节点 tree[i] 管辖区间:

[i−lowbit⁡(i)+1,i] [i-\operatorname{lowbit}(i)+1,i]

前缀查询通过 i -= lowbit(i) 把 [1,i][1,i] 拆成互不重叠的完整块;单点修改通过 i += lowbit(i) 枚举所有覆盖修改位置的块。

本篇保留这套块结构,只把块内运算从“求和”换成“取最大值”。

一句话算法

当一个位置只会变大时,把它沿树状数组更新路径取 max,查询前缀时再合并各块最大值。

问题模型

本篇以最大值为例,严格支持以下操作:

  1. 单调单点修改:
    apos←max⁡(apos,value) a_{pos}\leftarrow\max(a_{pos},value)
  2. 前缀查询:求
    max⁡(a1,a2,…,ar) \max(a_1,a_2,\ldots,a_r)

两种操作都要求在线完成。

适用边界

这里的修改是 chmax,不是任意赋值。若把某个位置从大值改小,旧最大值可能仍残留在多个树状数组节点中,简单更新无法删除它。

核心直觉

令每个节点保存自己管辖区间的最大值:

tree[i]=max⁡j=i−lowbit⁡(i)+1iaj tree[i]=\max_{j=i-\operatorname{lowbit}(i)+1}^{i}a_j

基础求和树状数组依赖两个性质:

  1. 一个点只影响 O(log⁡n)O(\log n) 个覆盖它的块;
  2. 一个前缀可以拆成 O(log⁡n)O(\log n) 个互不重叠的块。

取最大值同样可以合并这些块:

max⁡(A∪B)=max⁡(max⁡(A),max⁡(B)) \max(A\cup B)=\max(\max(A),\max(B))

而 chmax 修改不会让任何已有最大值失效。一个块的新最大值只可能是:

max⁡(旧块最大值,value) \max(\text{旧块最大值},value)

所以修改路径上直接取 max 即可。

算法步骤

初始化

树状数组节点初始化为负无穷。依次把初始数组加入:

cpp
        
1
2
3
for (int i = 1; i <= n; ++i) { bit.chmax(i, a[i]); }

单点 chmax

对 a[pos] = max(a[pos], value):

cpp
        
1
2
3
for (int i = pos; i <= n; i += lowbit(i)) { tree[i] = max(tree[i], value); }

这些节点正是所有覆盖 pos 的块。

查询前缀最大值

cpp
        
1
2
3
4
answer = negative_infinity; for (int i = right; i > 0; i -= lowbit(i)) { answer = max(answer, tree[i]); }

例如查询 [1,7][1,7]:

[1,7] = [7,7] + [5,6] + [1,4]

answer = max(tree[7], tree[6], tree[4])

小例子

初始数组为 [2,1,4,3,5][2,1,4,3,5]:

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] 始终等于其管辖区间的最大值。

  1. 初始化时,每个 aposa_{pos} 都沿更新路径加入所有覆盖它的块,因此每个块收集到区间内全部元素的最大值。
  2. 执行 chmax(pos, value) 时,只有覆盖 pos 的块可能变大。对这些块取 max(tree[i], value),恰好得到修改后的块最大值。
  3. 前缀查询选出的块两两不交并刚好覆盖 [1,r][1,r]。对这些块的最大值再次取最大值,就得到整个前缀的最大值。

因此,在单调修改条件下,修改和查询都正确。

复杂度分析

  • 初始化:O(nlog⁡n)O(n\log n);
  • 单点 chmax:O(log⁡n)O(\log n);
  • 前缀最大值查询:O(log⁡n)O(\log n);
  • 空间复杂度:O(n)O(n)。

代码模板

C++
cpp
        
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; } };
Python

只支持单点变大(chmax)与前缀最值:修改值变小会让已记录的最值失效。

python
        
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
# 单点 chmax、前缀最大值。 # 更新只能写成 a[pos] = max(a[pos], value),不能任意赋值:树状数组上的块 # 只维护"块内最大值",任意赋值无法下推到旧块,会破坏不变量。 # identity 取 -inf 对应 C++ 的 numeric_limits<T>::lowest()。 # 下标必须从 1 开始:lowbit(0) = 0,会让修改循环原地打转。 # 调用示例:fm = FenwickPrefixMax(5); fm.chmax(3, 7); fm.prefix_max(5) class FenwickPrefixMax: """树状数组维护前缀最大值:tree[i] 维护 a[i - lowbit(i) + 1 .. i] 的最大值。""" n: int identity: int tree: list[int] def __init__(self, size: int = 0) -> None: self.init(size) def init(self, size: int) -> None: """重置为长度 size 的空数组,所有位置先放 identity(即 -inf)。""" self.n = size # identity 必须满足“任何真实值都比它大”,取 -2**63 对应 # C++ 的 numeric_limits<long long>::lowest(),两边对拍时结果一致。 self.identity = -(2**63) self.tree = [self.identity] * (size + 1) @staticmethod def lowbit(x: int) -> int: """块长:x 二进制最低位那个 1 代表的值,也就是 x & -x。""" return x & -x def chmax(self, pos: int, value: int) -> None: """单点 chmax:a[pos] = max(a[pos], value)。""" i = pos while i <= self.n: if value > self.tree[i]: self.tree[i] = value i += self.lowbit(i) def prefix_max(self, pos: int) -> int: """前缀最大值:max(a[1], ..., a[pos]),每次并入一个以 i 结尾的完整块。 pos = 0 时返回 identity(空数组没有最大值,由调用方自行约定语义)。 """ answer = self.identity i = pos while i > 0: if self.tree[i] > answer: answer = self.tree[i] i -= self.lowbit(i) return answer

如果维护前缀最小值,把负无穷换成正无穷,并把所有 max 换成 min;对应修改必须是 chmin。

完整代码

本篇使用下面的自拟模板任务:

  • 1 x v:执行 a[x] = max(a[x], v);
  • 2 r:查询 [1,r][1,r] 的最大值。
C++
cpp
        
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; }
Python
python
        
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
65
66
67
68
69
# 树状数组维护前缀最大值:只支持单点「变大」+ 前缀 max。 # 下标从 1 开始,lowbit(0) = 0 会让循环原地打转。 # 不变量:chmax 只让值单调不减;一旦把某个位置改小,答案就错了。 # C++ 用 numeric_limits<long long>::lowest() 当单位元,这里取 -(2**63)。 import sys IDENTITY = -(1 << 63) class FenwickPrefixMax: """tree[i] 维护 a[i - lowbit(i) + 1 .. i] 的最大值。""" def __init__(self, size: int = 0) -> None: self.n = size self.tree = [IDENTITY] * (size + 1) @staticmethod def lowbit(x: int) -> int: # 最低位那个 1 代表的块长,即 x & -x。 return x & -x def chmax(self, pos: int, value: int) -> None: i = pos while i <= self.n: if value > self.tree[i]: self.tree[i] = value i += self.lowbit(i) def prefix_max(self, pos: int) -> int: answer = IDENTITY i = pos while i > 0: if self.tree[i] > answer: answer = self.tree[i] i -= self.lowbit(i) return answer def main() -> None: data = sys.stdin.buffer.read().split() if not data: return it = iter(map(int, data)) n = next(it) m = next(it) bit = FenwickPrefixMax(n) for i in range(1, n + 1): bit.chmax(i, next(it)) out: list[str] = [] for _ in range(m): operation = next(it) if operation == 1: pos = next(it) value = next(it) bit.chmax(pos, value) else: right = next(it) out.append(str(bit.prefix_max(right))) if out: sys.stdout.write("\n".join(out) + "\n") if __name__ == "__main__": main()

测试用例

输入:

5 5
2 1 4 3 5
2 4
1 2 6
2 3
1 5 1
2 5

输出:

4
6
6

易错点

  1. 任意赋值不满足单调条件,尤其不能用这份模板把较大值改小。
  2. max(prefix(r)) 与 max(prefix(l-1)) 不能相减,因此这份模板不能直接回答一般的 [l,r][l,r] 最大值。
  3. 初始值可能为负数,节点必须初始化为负无穷,不能默认初始化为 00。
  4. 一般静态区间最值优先使用 ST 表;支持任意单点修改时优先使用线段树。

主练习

实现本篇的自拟模板任务,并额外测试以下边界:

  1. 所有初始值均为负数;
  2. chmax 传入的值小于当前位置;
  3. 查询 r=1r=1 与 r=nr=n。

完成标准是:能够解释为什么 chmax 可以直接更新块最大值,而任意赋值不可以。

拓展题

luogu-P1198 题解

P1198 只在数组尾部追加元素,并查询最后若干项的最大值。它可以进一步利用树状数组块完成区间最大值查询,但需要保存原数组,并在完整块越过左端点时退化为单点处理。

这比本篇的“单调修改 + 前缀最大值”多一层区间拆分,因此放在完成主练习之后。