树状数组
You Asked:
template<typename T,int N=maxn>
struct Bit {
T c[N+5];
//Bit(){}
inline int lowbit(int x) { return x & -x; }
inline int fa(int p) { return p+lowbit(p); }
inline int left(int p) { return p-lowbit(p); }
void update(int p, T v){
for( ; p <= N; p = fa(p) ) c[p] += v;
}
T query(int p){ //前缀合
T sum=0;
for( ;p > 0 ; p = left(p)) sum+= c[p];
return sum;
}
};
Bit<ll> bit;
如何证明(数学), p+ lowbit(p) 就是父亲
Gemini Responded:
这是一个非常深刻且经典的问题。要理解为什么 p + lowbit(p) 是 p 的父亲(或者更准确地说是直接覆盖 p 的下一个区间节点),我们需要从二进制表示和树状数组(BIT)的区间定义两个角度来证明。
1. 核心定义:节点代表什么?
在树状数组中,每一个索引 t 不仅仅是一个下标,它代表一个区间。
对于任意节点 t,它维护的区间是:
更新操作(Update)的本质:
当我们更新位置 p 的值时,我们需要更新所有覆盖了位置 p 的节点。
也就是寻找所有满足以下条件的
(父节点索引必然大于等于子节点) (区间包含关系)
2. 数学证明:为什么 p + lowbit(p) 覆盖了 p
我们要证明:如果令
步骤 A:二进制表示
设 1 在第
步骤 B:执行加法
计算
步骤 C:分析
步骤 D:验证区间包含
节点
由
因为
这意味着:
3. 直观理解(二进制进位)
用一个具体的例子最容易理解。设
例子:更新位置 3
0011)
0001)
找父节点:
0100)
我们来看看这就发生了什么:
- 节点 3 (
0011) 的管辖长度是lowbit(3) = 1。区间是[3, 3]。 - 加法进位:
0011 + 0001 = 0100。末尾连续的 1 全部变成了 0,并在更高位产生了一个 1。 - 节点 4 (
0100) 的管辖长度是lowbit(4) = 4。区间是[1, 4]。 - 显然
[1, 4]包含3。
再往上找:
0100)
0100)
1000)
节点 8 的区间是 [1, 8],显然包含节点 4 的区间 [1, 4],也包含原始位置 3。
4. 总结
p + lowbit(p) 这个操作在数学上的意义是:
找到
lowbit(p):代表当前节点管辖的区域长度。 p + lowbit(p):把这个长度加回去,利用二进制进位,找到了一个更高位的、且能覆盖当前区域的节点。
这就是为什么在 update 操作中,我们不断执行 p += lowbit(p) 就能遍历所有包含位置
5. 下一步建议
既然你已经深入了解了 BIT 的数学原理,你是否需要我为你解释:
- 如何利用这个性质实现 区间修改 + 区间查询 (需要两个 BIT)?
- 或者对比一下 线段树 (Segment Tree),在什么情况下 BIT 的这种父子关系会失效或不够用?
You Asked:
这个证明复杂了,有没有 简洁一点的证明
Gemini Responded:
当然,我们可以从 “补齐” (Completion) 的角度给出一个极简的直觉证明。
核心直觉:二进制进位 = 积木合并
想象 BIT 的结构就是搭积木。
- 节点
代表一块长度为 lowbit(p)的积木。 - 这块积木放置在数轴的
位置。
证明逻辑如下:
-
现状:
的二进制末尾是一个 1后面跟着个 0(例如...0100)。 这意味着目前管辖着一段长度为 的区间。 -
目标: 我们要找
的父亲。父亲必须是一个更大的区间,并且这个大区间要包含 这个小区间。 -
操作: 做加法
。 这相当于在 的基础上,再加一段同样长度为 的积木。 -
结果: 二进制发生进位:
...0100+...0000(lowbit)...1000。 末尾的1消失了,进位到了更高位。这意味两块长度为
的小积木,合并成了一块长度为 的大积木。 这块新的大积木的右边界就是 ,它自然包含了原来的 。
一句话总结
这个大区间(父亲)当然包含原来的小区间(儿子)。
You Asked:
是不是要证明
Gemini Responded:
是的,你非常敏锐,这正是问题的关键!
证明“
换句话说,我们要证明:
在
简洁直观的证明:为什么中间的节点都“手太短”?
我们用区间长度来思考。
1. 设定的场景
- 令
。 - 假设有一个中间点
,满足 。
2. 关键推导:
的二进制末尾有 个 0(因为 )。 - 既然
只是增加了一个很小的量( ),那么 的二进制变化必然发生在这些末尾的 0 里面。 - 结论:
的末尾 0 的数量一定比 少。也就是说:
3. 判定覆盖:
让我们来看看
因为
回顾上面的结论:
既然
4. 最终结论
因为
举个例子验证 (N=8)
设 0100)。
我们来看看中间的数 5, 6, 7 是否包含 4?
-
( 0101):。 - 管辖区间:
。 - 左边界 5 > 4,不包含。
-
( 0110):。 - 管辖区间:
。 - 左边界 5 > 4,不包含。
-
( 0111):。 - 管辖区间:
。 - 左边界 7 > 4,不包含。
只有到了 8 (1000):
。 - 管辖区间
。 - 左边界 1 <= 4,包含!
总结
你说得对,证明不仅要是“包含”,还要是“紧邻”。
上面的推导证明了:在 lowbit 太小(手太短),都无法向左伸长去覆盖
代码
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
const int maxn = 1e5+5;
using ll = long long;
template<typename T,int N=maxn>
struct Bit {
T c[N+5]; // 树状数组, 1-based
//Bit(){}
inline int lowbit(int x) { return x & -x; } // lowbit
inline int fa(int p) { return p+lowbit(p); } // update a[p] 时, 下一个要更新的节点
inline int left(int p) { return p-lowbit(p); } // query a[1..p] 时, 下一个要求和的节点
// 单点更新 a[p] += v
void update(int p, T v){
for( ; p <= N; p = fa(p) ) c[p] += v;
}
// 查询前缀和 a[1..p]
T query(int p){ //前缀和
T sum=0;
for( ;p > 0 ; p = left(p)) sum+= c[p];
return sum;
}
};
Bit<ll> bit;
区间修改,单点查询
一句话算法
就是差分数组做BIT
区间修改,单点查询,在差分数组上就是单点修改,区间查询
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
const int maxn = 1e5+5;
using ll = long long;
template<typename T,int N=maxn>
struct Bit {
T c[N+5]; // 树状数组, 1-based
//Bit(){}
inline int lowbit(int x) { return x & -x; } // lowbit
inline int fa(int p) { return p+lowbit(p); } // update a[p] 时, 下一个要更新的节点
inline int left(int p) { return p-lowbit(p); } // query a[1..p] 时, 下一个要求和的节点
// 单点更新 a[p] += v
void update(int p, T v){
for( ; p <= N; p = fa(p) ) c[p] += v;
}
// 查询前缀和 a[1..p]
T query(int p){ //前缀和
T sum=0;
for( ;p > 0 ; p = left(p)) sum+= c[p];
return sum;
}
// 区间和
T query(int l, int r){
return query(r) - query(l-1);
}
// 区间加法 a[l..r] += v
void range_add(int l, int r, T v){
update(l, v);
update(r+1, -v);
}
};
Bit<ll> bit;
树状数组应用分类详解
树状数组 (Binary Indexed Tree, BIT) 是一种精巧的数据结构,其本质是利用二进制的位运算特性,高效地维护序列的前缀信息。它能在对数时间内完成单点修改和前缀查询,是解决动态区间问题的利器。本文将树状数组的应用场景系统地分为四大类别。
一、基础操作:单点修改与区间查询
核心操作: update(i, val)、query(i)
典型模式: 维护一个动态序列,快速响应单点变化并查询区间累计值。
1.1 动态前缀和与区间和
这是树状数组最核心、最直接的应用。
- 动态前缀和:在
时间内修改序列中的单个元素,并查询任意位置的前缀和。 - 动态区间和:基于前缀和,通过
query(r) - query(l-1)的方式,可以快速得到任意区间[l, r]的和。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 单点更新, 区间求和 | Luogu P3374 | update 修改点值,query(r) - query(l-1) 求区间和 |
经典实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 单点增加 val
void update(int p, int val) {
for (; p <= N; p += lowbit(p)) c[p] += val;
}
// 查询 [1, p] 的前缀和
int query(int p) {
int sum = 0;
for (; p > 0; p -= lowbit(p)) sum += c[p];
return sum;
}
// 查询区间 [l, r] 的和
int range_query(int l, int r) {
return query(r) - query(l - 1);
}
二、核心技巧:差分思想
核心思路: 将对原数组的区间操作,转化为对差分数组的单点操作。
2.1 区间修改与单点查询
通过维护原数组的差分数组,可以巧妙地处理区间修改问题。
- 区间修改
[l, r]:转化为对差分数组的两次单点修改:diff.update(l, val)和diff.update(r + 1, -val)。 - 单点查询
a[i]:原数组的a[i]等价于差分数组的前缀和diff.query(i)。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 区间批量加/减 | Luogu P3368 | 维护差分数组,区间修改 -> 两次单点修改 |
经典实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 差分树状数组
Bit diff_bit;
// 对原数组 [l, r] 区间增加 val
void range_add(int l, int r, int val) {
diff_bit.update(l, val);
diff_bit.update(r + 1, -val);
}
// 查询原数组 a[p] 的值
int point_query(int p) {
return diff_bit.query(p);
}
三、进阶应用:值域统计
核心思路: 将树状数组建立在值的范围(值域)上,而非数组下标,用于统计频率和排名。
3.1 逆序对与排名查询
当处理的数据范围较大但数量不多时,通常需要先进行离散化。
- 逆序对统计:从后往前遍历序列,每遇到一个数
x,就将其加入值域树状数组,并查询在它之前已加入的、比x小的数的个数。 - 动态查询排名:统计动态序列中“小于等于
k的数有多少个”这类问题。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 逆序对统计 | Luogu P1908 | 值域树状数组 + 离散化 |
| 动态排名 | Luogu P3369 (平衡树模板) | 权值线段树/平衡树更优,但BIT可解部分问题 |
四、高级拓展:升维与变形
核心思路: 将一维 BIT 的思想扩展到更高维度或更复杂的数据结构上。
4.1 二维树状数组
将一维 BIT 扩展到二维,用于维护矩阵信息。
- 单点修改
(x, y):在两个维度上同时执行+= lowbit()循环。 - 查询子矩阵和:利用二维前缀和的容斥原理进行查询。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 二维矩阵区域和 | (模板题) | c[x][y] 维护矩阵,操作复杂度 |
4.2 树上树状数组
结合 DFS 序,将对树的操作转化为对序列的操作。
- 子树操作:一棵子树在 DFS 序上对应一个连续区间,对子树的修改和查询可直接转化为对该区间的操作。
- 路径操作:利用差分思想,将对
u到v路径的操作,转化为对u,v,lca(u,v),fa(lca(u,v))四个点的单点修改。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 子树信息维护 | Luogu P3372 (线段树模板) | DFS 序将子树问题转化为区间问题 |
应用分类总览
| 分类 | 核心特征 | 时间复杂度 | 适用场景 |
|---|---|---|---|
| 基础操作 | 动态维护前缀和 | O(log N) | 单点修改、区间查询 |
| 差分技巧 | 区间修改转单点 | O(log N) | 区间修改、单点查询 |
| 值域统计 | 建立在值域上 | O(log V) (V为值域大小) | 逆序对、排名、计数 |
| 高级拓展 | 升维或结合树结构 | O(log²N) 或 O(log N) | 二维矩阵、子树/路径问题 |
练习建议
- 入门模板: Luogu P3374 (单点修改, 区间查询)
- 差分思想: Luogu P3368 (区间修改, 单点查询)
- 值域应用: Luogu P1908 (逆序对)
掌握这四大分类,你就能灵活运用树状数组解决大部分相关的算法问题。