This commit is contained in:
Zengtudor 2024-08-13 10:18:54 +08:00
parent 8282453b9f
commit 9150dd04d9
3 changed files with 237 additions and 1 deletions

View File

@ -108,3 +108,7 @@ ll ksm(ll a,ll b,ll M){
## Day9
### 字典树与异或极值
## Day11
[树状数组MD](./day11/FenwickTree/FenwickTree.md)
[树状数组PDF](./day11/FenwickTree/FenwickTree.pdf)

View File

@ -219,3 +219,235 @@ int sum = query(6, bit) - query(2, bit);
### 总结
我们需要使用 `l - 1` 是因为 `query(r, bit)` 计算的是从 `1``r` 的前缀和,而 `query(l - 1, bit)` 则是计算从 `1``l-1` 的前缀和。通过计算差值,我们得到了 `[l, r]` 区间的和。
在树状数组中,区间修改(即对数组中一段连续的区间进行统一的增加或减少)通常是通过一些技巧来实现的。由于传统的树状数组主要支持单点更新和前缀和查询,要支持区间修改,我们可以采用一些特殊的设计思路。
### 1. 传统树状数组的局限性
通常,树状数组适合以下操作:
- **单点更新**:即在数组的某个位置 `i` 上加上或减去一个值 `delta`
- **前缀和查询**:查询从数组开始到某个位置 `i` 的所有元素的和。
然而,直接使用树状数组并不容易支持区间修改(例如同时对 `[l, r]` 区间内的所有元素增加 `delta`)。
### 2. 双树状数组解决方案
为了实现区间修改,可以使用**两个树状数组**来分别维护差分数组。我们可以通过这两个树状数组来支持区间的加减操作。
#### 思路
考虑一个数组 `arr`,我们希望对其进行区间修改(比如对 `[l, r]` 区间内的所有元素增加一个值 `delta`)。我们可以引入两个树状数组 `B1``B2` 来分别维护 `arr` 的差分信息:
- 树状数组 `B1` 用于处理与位置 `i` 成正比的影响。
- 树状数组 `B2` 用于处理常数影响。
#### 如何更新和查询
1. **区间更新**:对 `[l, r]` 区间增加 `delta`,分两步来做:
- 更新 `B1` 数组:`B1[l] += delta``B1[r+1] -= delta`。
- 更新 `B2` 数组:`B2[l] += delta * (l-1)``B2[r+1] -= delta * r`。
具体实现:
```cpp
void range_update(int l, int r, int delta, vector<int>& B1, vector<int>& B2, int n) {
// 更新 B1
update(l, delta, n, B1);
update(r + 1, -delta, n, B1);
// 更新 B2
update(l, delta * (l - 1), n, B2);
update(r + 1, -delta * r, n, B2);
}
```
2. **前缀和查询**:查询前缀和 `sum(1...i)` 可以通过以下方式获得:
- `sum(1...i) = query(i, B1) * i - query(i, B2)`
具体实现:
```cpp
int prefix_sum(int i, vector<int>& B1, vector<int>& B2) {
return query(i, B1) * i - query(i, B2);
}
```
3. **区间查询**:查询区间 `[l, r]` 的和:
- `sum(l...r) = prefix_sum(r) - prefix_sum(l-1)`
具体实现:
```cpp
int range_query(int l, int r, vector<int>& B1, vector<int>& B2) {
return prefix_sum(r, B1, B2) - prefix_sum(l - 1, B1, B2);
}
```
#### 代码示例
以下是完整的代码示例,包含区间修改和区间查询的实现:
```cpp
#include <iostream>
#include <vector>
using namespace std;
// 单点更新函数
void update(int i, int delta, int n, vector<int>& bit) {
while (i <= n) {
bit[i] += delta;
i += i & (-i);
}
}
// 单点查询函数
int query(int i, vector<int>& bit) {
int sum = 0;
while (i > 0) {
sum += bit[i];
i -= i & (-i);
}
return sum;
}
// 前缀和查询函数
int prefix_sum(int i, vector<int>& B1, vector<int>& B2) {
return query(i, B1) * i - query(i, B2);
}
// 区间和查询函数
int range_query(int l, int r, vector<int>& B1, vector<int>& B2) {
return prefix_sum(r, B1, B2) - prefix_sum(l - 1, B1, B2);
}
// 区间更新函数
void range_update(int l, int r, int delta, vector<int>& B1, vector<int>& B2, int n) {
update(l, delta, n, B1);
update(r + 1, -delta, n, B1);
update(l, delta * (l - 1), n, B2);
update(r + 1, -delta * r, n, B2);
}
int main() {
int n = 8; // 数组长度
vector<int> B1(n + 1, 0), B2(n + 1, 0); // 初始化两个树状数组
// 对区间 [2, 5] 增加 3
range_update(2, 5, 3, B1, B2, n);
// 查询区间 [1, 5] 的和
cout << "Sum of range [1, 5]: " << range_query(1, 5, B1, B2) << endl; // 输出应为 12
return 0;
}
```
### 总结
通过使用两个树状数组,我们可以有效地实现区间更新(即对一个区间内的所有元素增加或减少某个值)和区间查询操作。这种方法的核心思想是利用差分数组的概念来维护区间的影响,从而在树状数组的基础上扩展了其功能。
在树状数组中使用“差分乘以位置”的技巧,主要是为了将区间更新转化为树状数组能够处理的前缀和查询。这个技巧通过两个树状数组的配合,确保在进行区间更新后,我们仍然可以高效地计算任意区间的和。
### 1. 问题背景
我们希望能够对数组 `arr` 的某个区间 `[l, r]` 内的所有元素同时加上一个值 `delta`,并且在之后能够快速地查询任意区间的和。如果直接对每个元素单独加上 `delta`,那么每次区间更新的复杂度将会是 \(O(n)\),这对于大数组来说非常不高效。因此,我们需要一种巧妙的方式来保持操作的效率。
### 2. 差分数组与位置的关系
回顾差分数组的原理:对于一个数组 `arr`,它的差分数组 `d` 是这样定义的:
\[ d[i] = arr[i] - arr[i-1] \]
如果我们对区间 `[l, r]` 的所有元素加上一个值 `delta`,差分数组会进行如下修改:
- `d[l] += delta`:表示从位置 `l` 开始,所有元素增加 `delta`
- `d[r+1] -= delta`:表示从位置 `r+1` 开始,恢复原来的数值(即对冲 `delta` 的影响)。
通过这个差分数组,我们可以将区间更新的影响从每个单独的元素转移到两个点上,从而使更新操作的复杂度降为 \(O(\log n)\)。
### 3. 为什么需要“差分乘以位置”
然而,仅仅使用一个差分数组并不足以处理查询区间和的问题。为了解决这个问题,我们使用两个树状数组:
- 一个用于存储基本的差分(`B1`)。
- 另一个用于存储差分乘以位置的结果(`B2`)。
这里的“差分乘以位置”其实是为了在查询时恢复出正确的区间和。具体来说,使用差分乘以位置的原因如下:
#### 1. 维持累加效应的平衡
假设我们在区间 `[l, r]` 增加 `delta`,我们希望每个位置 `i`(其中 `l ≤ i ≤ r`)都增加 `delta`。考虑查询前缀和时:
- 如果仅仅使用 `B1` 来存储差分数组,我们得到的是每个元素的差分累积量,这个累积量随着 `i` 增加。
- 然而,如果我们想要恢复出真正的区间和,需要考虑这些差分对每个元素的累积影响。这时候,需要在 `B2` 中存储差分乘以位置的结果,这样在计算前缀和时就能抵消不必要的多加部分。
#### 2. 构建正确的前缀和公式
假设我们希望查询区间 `[1, i]` 的和:
- 直接使用 `B1` 的查询结果(即 `query(i, B1) * i`)会累加过多的影响,因为 `query(i, B1)` 实际上只考虑到了基本差分,而没有考虑累积效应的差别。
- 通过减去 `query(i, B2)`,我们就可以剔除多余的部分,得到正确的区间和。
因此,前缀和的计算公式为:
\[ \text{prefix\_sum}(i) = i \cdot \text{query}(i, B1) - \text{query}(i, B2) \]
这个公式通过引入 `B2` 来校正查询结果,确保我们得到的前缀和是准确的。
### 4. 具体作用
- `B1` 负责记录直接的差分变化。
- `B2` 通过存储“差分乘以位置”的值,帮助校正前缀和中的多余累积,使得前缀和查询能够返回正确的值。
### 5. 总结
“差分乘以位置”的引入,是为了确保在树状数组处理区间更新时,我们能够正确计算任意前缀和或区间和。通过使用两个树状数组,一个记录基础差分,另一个记录差分乘以位置,我们可以在 \(O(\log n)\) 时间内实现区间更新和区间查询,保持了树状数组的高效性。在树状数组中使用“差分乘以位置”的技巧,主要是为了将区间更新转化为树状数组能够处理的前缀和查询。这个技巧通过两个树状数组的配合,确保在进行区间更新后,我们仍然可以高效地计算任意区间的和。
### 1. 问题背景
我们希望能够对数组 `arr` 的某个区间 `[l, r]` 内的所有元素同时加上一个值 `delta`,并且在之后能够快速地查询任意区间的和。如果直接对每个元素单独加上 `delta`,那么每次区间更新的复杂度将会是 \(O(n)\),这对于大数组来说非常不高效。因此,我们需要一种巧妙的方式来保持操作的效率。
### 2. 差分数组与位置的关系
回顾差分数组的原理:对于一个数组 `arr`,它的差分数组 `d` 是这样定义的:
\[ d[i] = arr[i] - arr[i-1] \]
如果我们对区间 `[l, r]` 的所有元素加上一个值 `delta`,差分数组会进行如下修改:
- `d[l] += delta`:表示从位置 `l` 开始,所有元素增加 `delta`
- `d[r+1] -= delta`:表示从位置 `r+1` 开始,恢复原来的数值(即对冲 `delta` 的影响)。
通过这个差分数组,我们可以将区间更新的影响从每个单独的元素转移到两个点上,从而使更新操作的复杂度降为 \(O(\log n)\)。
### 3. 为什么需要“差分乘以位置”
然而,仅仅使用一个差分数组并不足以处理查询区间和的问题。为了解决这个问题,我们使用两个树状数组:
- 一个用于存储基本的差分(`B1`)。
- 另一个用于存储差分乘以位置的结果(`B2`)。
这里的“差分乘以位置”其实是为了在查询时恢复出正确的区间和。具体来说,使用差分乘以位置的原因如下:
#### 1. 维持累加效应的平衡
假设我们在区间 `[l, r]` 增加 `delta`,我们希望每个位置 `i`(其中 `l ≤ i ≤ r`)都增加 `delta`。考虑查询前缀和时:
- 如果仅仅使用 `B1` 来存储差分数组,我们得到的是每个元素的差分累积量,这个累积量随着 `i` 增加。
- 然而,如果我们想要恢复出真正的区间和,需要考虑这些差分对每个元素的累积影响。这时候,需要在 `B2` 中存储差分乘以位置的结果,这样在计算前缀和时就能抵消不必要的多加部分。
#### 2. 构建正确的前缀和公式
假设我们希望查询区间 `[1, i]` 的和:
- 直接使用 `B1` 的查询结果(即 `query(i, B1) * i`)会累加过多的影响,因为 `query(i, B1)` 实际上只考虑到了基本差分,而没有考虑累积效应的差别。
- 通过减去 `query(i, B2)`,我们就可以剔除多余的部分,得到正确的区间和。
因此,前缀和的计算公式为:
\[ \text{prefix\_sum}(i) = i \cdot \text{query}(i, B1) - \text{query}(i, B2) \]
这个公式通过引入 `B2` 来校正查询结果,确保我们得到的前缀和是准确的。
### 4. 具体作用
- `B1` 负责记录直接的差分变化。
- `B2` 通过存储“差分乘以位置”的值,帮助校正前缀和中的多余累积,使得前缀和查询能够返回正确的值。
### 5. 总结
“差分乘以位置”的引入,是为了确保在树状数组处理区间更新时,我们能够正确计算任意前缀和或区间和。通过使用两个树状数组,一个记录基础差分,另一个记录差分乘以位置,我们可以在 \(O(\log n)\) 时间内实现区间更新和区间查询,保持了树状数组的高效性。

Binary file not shown.