diff --git a/day11/FenwickTree/FenwickTree.md b/day11/FenwickTree/FenwickTree.md new file mode 100644 index 0000000..3f70e31 --- /dev/null +++ b/day11/FenwickTree/FenwickTree.md @@ -0,0 +1,221 @@ +树状数组(Fenwick Tree),也称为二叉索引树(Binary Indexed Tree,BIT),是一种用于处理前缀和或区间查询问题的数据结构。它的主要作用如下: + +1. **高效计算前缀和**:树状数组能够在 \(O(\log n)\) 的时间复杂度内计算前缀和,即数组中从第一个元素到指定位置的所有元素的和。 + +2. **高效更新元素**:树状数组能够在 \(O(\log n)\) 的时间复杂度内更新数组中的某个元素。更新后,树状数组会自动维护相应的前缀和。 + +3. **支持动态查询和修改**:与直接维护前缀和的简单数组不同,树状数组支持在不重新计算整个数组的情况下进行动态的查询和修改操作。 + +### 树状数组的应用场景 +- **动态数组的区间和查询**:在动态更新数组元素的情况下,快速求解某一前缀的和,或是两个下标间元素的区间和。 +- **求逆序对**:在计算一个数组中的逆序对数量时,树状数组可以用于高效计算逆序对的个数。 +- **数列的动态排序**:在需要动态排序的场景下,可以使用树状数组来维护当前数列的顺序,快速找到插入位置等操作。 + +### 树状数组的基本操作 +1. **单点更新**:更新数组中某个位置的值,并更新与之相关的前缀和。 +2. **前缀和查询**:查询从第一个元素到某个位置的和。 +3. **区间和查询**:通过两次前缀和查询得到区间和,即 \( \text{sum}(l, r) = \text{sum}(r) - \text{sum}(l-1) \)。 + +树状数组的设计使得它在很多需要频繁查询与更新的场景中,成为一种非常高效的工具。 + +树状数组的基本操作主要包括**单点更新**和**前缀和查询**。下面将用C++详细讲解这两种操作的实现。 + +### 1. 树状数组的基本结构 +树状数组使用一个数组来存储信息,常用的表示方式是 `bit[]`,其中 `bit[i]` 表示在树状数组中与原数组 `arr[i]` 相关的某个区间的和。 + +树状数组的大小通常为 `n+1`,其中 `n` 是原数组的长度。索引 `0` 通常不使用,数组从索引 `1` 开始。 + +### 2. 单点更新操作 +假设我们要将原数组的某个位置 `i` 的值增加 `delta`,树状数组的更新操作如下: + +```cpp +void update(int i, int delta, int n, vector& bit) { + while (i <= n) { + bit[i] += delta; + i += i & (-i); + } +} +``` + +#### 解释: +- `i` 是要更新的位置。 +- `delta` 是要增加的值。 +- `n` 是数组的大小。 +- `bit[]` 是存储树状数组的数组。 + +**更新过程**: +- 每次更新时,`bit[i]` 的值增加 `delta`。 +- 然后通过 `i += i & (-i)` 移动到树状数组的下一个位置进行更新。`i & (-i)` 计算出 `i` 的最低位 1 的值,对应于二进制中最低有效位。 + +例如,假设 `i = 5`,则 `i` 对应二进制为 `101`,`i & (-i)` 计算出的值为 `1`,表示更新与位置 `i` 相关的区间和。 + +### 3. 前缀和查询操作 +假设我们要查询从数组起始位置到 `i` 的前缀和: + +```cpp +int query(int i, vector& bit) { + int sum = 0; + while (i > 0) { + sum += bit[i]; + i -= i & (-i); + } + return sum; +} +``` + +#### 解释: +- `i` 是查询的终止位置。 +- `bit[]` 是存储树状数组的数组。 + +**查询过程**: +- 从 `i` 开始,累加 `bit[i]` 的值到 `sum`。 +- 然后通过 `i -= i & (-i)` 移动到前一个区间,继续累加。 +- 直到 `i` 减少到 0 时,返回累加的 `sum` 值。 + +### 4. 区间和查询操作 +区间 `[l, r]` 的和可以通过两次前缀和查询得到: + +```cpp +int rangeQuery(int l, int r, vector& bit) { + return query(r, bit) - query(l - 1, bit); +} +``` + +#### 解释: +- `query(r)` 返回从起始位置到 `r` 的前缀和。 +- `query(l - 1)` 返回从起始位置到 `l-1` 的前缀和。 +- 两者相减即为 `[l, r]` 的区间和。 + +### 5. 完整示例 +下面是一个完整的示例,演示如何使用树状数组进行更新和查询操作: + +```cpp +#include +#include + +using namespace std; + +// 更新操作:在位置 i 增加 delta +void update(int i, int delta, int n, vector& bit) { + while (i <= n) { + bit[i] += delta; + i += i & (-i); + } +} + +// 前缀和查询:查询前缀和 [1, i] +int query(int i, vector& bit) { + int sum = 0; + while (i > 0) { + sum += bit[i]; + i -= i & (-i); + } + return sum; +} + +// 区间和查询:[l, r] 的区间和 +int rangeQuery(int l, int r, vector& bit) { + return query(r, bit) - query(l - 1, bit); +} + +int main() { + int n = 10; // 数组大小 + vector arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 原数组 + vector bit(n + 1, 0); // 树状数组初始化为 0 + + // 建立树状数组 + for (int i = 1; i <= n; ++i) { + update(i, arr[i], n, bit); + } + + // 查询 [1, 5] 的前缀和 + cout << "Sum of range [1, 5]: " << rangeQuery(1, 5, bit) << endl; + + // 更新位置 3,增加 6 + update(3, 6, n, bit); + cout << "Sum of range [1, 5] after update: " << rangeQuery(1, 5, bit) << endl; + + return 0; +} +``` + +### 6. 运行结果 +该程序首先建立树状数组,然后查询 `[1, 5]` 的区间和,输出 `15`(即 `1+2+3+4+5`)。随后将位置 `3` 的值增加 `6`,再次查询 `[1, 5]` 的区间和,输出 `21`(即 `1+2+9+4+5`)。 + +通过上述示例,你可以看到树状数组在处理前缀和和区间和查询时的高效性。 + +查询 `i` 的最低有效位在树状数组中的作用是**确定哪些区间需要更新**或**哪些区间在查询时需要累加**。这背后涉及到树状数组的结构设计,具体来说,树状数组利用了区间的二进制表示来快速确定更新或查询的范围。 + +### 树状数组的结构和区间表示 +树状数组的核心思想是使用一个一维数组来有效地管理前缀和。对于一个数组 `arr`,树状数组通过一个额外的数组 `bit` 来存储某些区间的和,这样可以在 \(O(\log n)\) 的时间内完成更新和查询。 + +#### 树状数组如何划分区间 +树状数组中的每个索引 `i` 代表了一个从 `i` 开始的区间,其长度是由 `i` 的最低有效位(LSB)决定的。例如: + +- `i = 4` (二进制 `100`) 代表的区间为 `[4, 4]`,即它只包含自己。 +- `i = 6` (二进制 `110`) 代表的区间为 `[5, 6]`。 +- `i = 8` (二进制 `1000`) 代表的区间为 `[1, 8]`,包含从 `1` 到 `8` 的所有元素。 + +这种划分是通过 `i` 的二进制表示及其最低有效位来实现的。例如: +- **最低有效位为 `1` 的数**:`i = 1, 2, 4, 8, ...` 代表的区间是从 `i` 开始到 `i` 结束的区间,这些数只有一个元素。 +- **最低有效位为 `2` 的数**:`i = 3, 6, 12, ...` 代表的区间是从 `i - 1` 开始,到 `i` 结束的区间,这些数包含两个元素。 + +### 为什么要查询 `i` 的最低有效位 +通过查询 `i` 的最低有效位,可以快速确定与 `i` 对应的区间以及哪些索引与其关联: + +1. **更新操作**: + - 当你在位置 `i` 更新一个值时,需要更新所有包含该位置的区间的和。这些区间的索引就是根据 `i` 的最低有效位来计算的。 + - 例如,当你更新 `i = 3` 时,你需要更新区间 `[3]`(由 `3` 本身表示)和区间 `[2, 3]`(由 `4` 表示),通过 `i += i & (-i)` 来跳到下一个区间并继续更新。 + +2. **查询操作**: + - 当你查询前缀和时,需要累加所有包含在 `[1, i]` 中的区间的和。这些区间同样是通过 `i` 的最低有效位来确定的。 + - 例如,当你查询 `i = 6` 时,你需要累加 `[5, 6]`(由 `6` 表示)和 `[1, 4]`(由 `4` 表示)等区间的和。通过 `i -= i & (-i)`,你可以跳到前一个区间继续累加。 + +### 最低有效位的关键作用 +通过 `i` 的最低有效位,树状数组能够实现以下两个关键功能: + +1. **快速跳转到下一个相关区间**:在更新时,通过 `i += i & (-i)`,你可以快速找到下一个需要更新的区间索引,而不是逐一遍历所有索引。 +2. **快速确定累加范围**:在查询时,通过 `i -= i & (-i)`,你可以快速跳转到下一个需要累加的区间索引,避免遍历所有可能的区间。 + +**总结**: +- 查询 `i` 的最低有效位帮助确定当前索引对应的区间大小和范围,使得树状数组能够高效地进行更新和查询操作。 +- 这种设计利用了二进制的特性,确保每个操作的时间复杂度为 \(O(\log n)\),大大提高了处理大规模数据时的效率。 +在使用树状数组查询区间 `[l, r]` 的和时,我们通常会通过以下方法来计算: + +\[ \text{Sum of range } [l, r] = \text{query}(r, \text{bit}) - \text{query}(l - 1, \text{bit}) \] + +### 1. 树状数组中的前缀和查询 + +树状数组的查询函数 `query(i, bit)` 通常返回从数组的起始位置 `1` 到位置 `i` 的前缀和,即: + +\[ \text{query}(i, \text{bit}) = \text{Sum from } 1 \text{ to } i \] + +这意味着 `query(i, bit)` 给出的结果是从索引 `1` 到 `i` 的和。如果我们想计算任意区间 `[l, r]` 的和,我们需要减去 `[1, l-1]` 的和,以排除掉不在区间 `[l, r]` 中的部分。 + +### 2. 为什么要用 `l - 1` + +计算区间 `[l, r]` 的和时,我们要得到的结果是: + +\[ \text{Sum from } l \text{ to } r = \text{Sum from } 1 \text{ to } r - \text{Sum from } 1 \text{ to } (l - 1) \] + +- **`query(r, bit)`** 返回 `[1, r]` 的和。 +- **`query(l - 1, bit)`** 返回 `[1, l-1]` 的和。 + +通过减去 `[1, l-1]` 的和,我们就得到从 `l` 到 `r` 的和,因为 `[1, r]` 的和包含了 `[1, l-1]` 的部分,而我们只需要 `[l, r]`。 + +### 3. 示例解释 + +假设我们有一个数组 `arr`,树状数组 `bit` 已经建立好。我们想要计算区间 `[3, 6]` 的和: + +```cpp +int sum = query(6, bit) - query(2, bit); +``` + +- `query(6, bit)` 返回的是 `[1, 6]` 的和,假设它的值是 `21`。 +- `query(2, bit)` 返回的是 `[1, 2]` 的和,假设它的值是 `3`。 + +那么 `21 - 3` 就是 `[3, 6]` 的和,即 `18`。 + +### 总结 + +我们需要使用 `l - 1` 是因为 `query(r, bit)` 计算的是从 `1` 到 `r` 的前缀和,而 `query(l - 1, bit)` 则是计算从 `1` 到 `l-1` 的前缀和。通过计算差值,我们得到了 `[l, r]` 区间的和。 \ No newline at end of file diff --git a/day11/P3374/1.in b/day11/P3374/1.in new file mode 100644 index 0000000..d07382e --- /dev/null +++ b/day11/P3374/1.in @@ -0,0 +1,7 @@ +5 5 +1 5 4 2 3 +1 1 3 +2 2 5 +1 3 -1 +1 4 2 +2 1 4 \ No newline at end of file diff --git a/day11/P3374/P3374.cpp b/day11/P3374/P3374.cpp new file mode 100644 index 0000000..f7ba9eb --- /dev/null +++ b/day11/P3374/P3374.cpp @@ -0,0 +1,70 @@ +#include +using namespace std; +#define int long long + +int lowbit(int x); +void update(int i,int n,int num,vector &a); +int query(int i,vector &a); +int rangeQuery(int l,int r,vector &a); +int readint(); + +signed main(){ + int n=readint(),m=readint(); + vector a(n+1,0); + for(int i=1;i<=n;i++){ + int num; + cin>>num; + update(i, n, num, a); + } + for(int i=1;i<=m;i++){ + const int o = readint(); + if(o==1){ + const int x=readint(),k=readint(); + update(x, n, k, a); + }else{ + const int x=readint(),y=readint(); + cout< &a){ + return query(r, a) - query(l-1, a); +} + +int query(int i,vector &a){ + int sum=0; + while(i>0){ + sum+=a[i]; + i-=lowbit(i); + } + return sum; +} + +void update(int i,int n,int num,vector &a){ + while(i<=n){ + a[i]+=num; + i+=lowbit(i); + } +} + +int lowbit(int x){ + return x&(-x); +} \ No newline at end of file diff --git a/xmake.lua b/xmake.lua index f61fcd6..49c9a1a 100644 --- a/xmake.lua +++ b/xmake.lua @@ -116,4 +116,8 @@ target("U178578") target("P9127") set_rundir("./day9/P9127") - add_files("./day9/P9127/*.cpp") \ No newline at end of file + add_files("./day9/P9127/*.cpp") + +target("P3374") + set_rundir("./day11/P3374/P3374.cpp") + add_files("./day11/P3374/*.cpp") \ No newline at end of file