diff --git a/README.md b/README.md index 1b807ae..74267d7 100644 --- a/README.md +++ b/README.md @@ -108,3 +108,7 @@ ll ksm(ll a,ll b,ll M){ ## Day9 ### 字典树与异或极值 + +## Day11 +[树状数组MD](./day11/FenwickTree/FenwickTree.md) +[树状数组PDF](./day11/FenwickTree/FenwickTree.pdf) \ No newline at end of file diff --git a/day11/FenwickTree/FenwickTree.md b/day11/FenwickTree/FenwickTree.md index 3f70e31..ce1623a 100644 --- a/day11/FenwickTree/FenwickTree.md +++ b/day11/FenwickTree/FenwickTree.md @@ -218,4 +218,236 @@ int sum = query(6, bit) - query(2, bit); ### 总结 -我们需要使用 `l - 1` 是因为 `query(r, bit)` 计算的是从 `1` 到 `r` 的前缀和,而 `query(l - 1, bit)` 则是计算从 `1` 到 `l-1` 的前缀和。通过计算差值,我们得到了 `[l, r]` 区间的和。 \ No newline at end of file +我们需要使用 `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& B1, vector& 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& B1, vector& 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& B1, vector& B2) { + return prefix_sum(r, B1, B2) - prefix_sum(l - 1, B1, B2); + } + ``` + +#### 代码示例 + +以下是完整的代码示例,包含区间修改和区间查询的实现: + +```cpp +#include +#include + +using namespace std; + +// 单点更新函数 +void update(int i, int delta, int n, vector& bit) { + while (i <= n) { + bit[i] += delta; + i += i & (-i); + } +} + +// 单点查询函数 +int query(int i, vector& bit) { + int sum = 0; + while (i > 0) { + sum += bit[i]; + i -= i & (-i); + } + return sum; +} + +// 前缀和查询函数 +int prefix_sum(int i, vector& B1, vector& B2) { + return query(i, B1) * i - query(i, B2); +} + +// 区间和查询函数 +int range_query(int l, int r, vector& B1, vector& B2) { + return prefix_sum(r, B1, B2) - prefix_sum(l - 1, B1, B2); +} + +// 区间更新函数 +void range_update(int l, int r, int delta, vector& B1, vector& 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 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)\) 时间内实现区间更新和区间查询,保持了树状数组的高效性。 \ No newline at end of file diff --git a/day11/FenwickTree/FenwickTree.pdf b/day11/FenwickTree/FenwickTree.pdf new file mode 100644 index 0000000..f977c4b Binary files /dev/null and b/day11/FenwickTree/FenwickTree.pdf differ