%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # 一维前缀和数组 ## 前缀和数组的定义 给定数组 `arr`,其**前缀和数组**的定义有两种方式。 - 方式一:`prefix_sum[i]` 表示数组 `arr` 中**区间 `[0, i]` 的前缀和**(**前`i+1`项**元素之和) - 方式二:`prefix_sum[i]` 表示数组 `arr` 中**区间 `[0, i)` 的前缀和**(**==前`i` 项==** 元素之和) - 特别的,有 **==哨兵边界== `prefix_sum[0]`** 表示 0 项元素之和,即为 0. 如下图所示: ![[Excalidraw-Solution/前缀和与差分数组_1.excalidraw.svg|839]] %%[[Excalidraw-Solution/前缀和与差分数组_1.excalidraw.md|🖋 Edit in Excalidraw]]%% 方式二相较于方式一的优点在于 "**统一了区间和的计算逻辑**" : - 第一种方式中,**计算 "区间和" 时需要先==判断左边界 `l` 是否为`0`==**,对应不同逻辑; - 第二种方式中,`[l, r]` 的**区间和** 统一为 `prefix_sum[r + 1] - prefix_sum[l]` 。 <br> ## 前缀和数组的构建 ### 定义方式一 - `prefix_sum[i]` 表示数组 `arr` 中**区间 `[0, i]` 的前缀和**(**前`i+1`项**元素之和) - `prefix_sum` 长度为 `n`; - 构建前缀和数组: - `prefix_sum[0] = arr[0]`; - `prefix_sum[i] = prefix_sum[i - 1] + arr[i]`,对 `i = [1, n-1]` - `[l, r]` 的区间和为:(需要分类讨论 `l==0` 与 `l!=0` ) - `if l == 0`,即为`prefix_sum[l]`; - `if l != 0`,则为 `prefix_sum[r] - prefix_sum[l-1]`; ```cpp // 前缀和数组 // 定义一: `prfixe_sum[i]` 表示下标区间 `[0, i]` 的前 `i+1` 项元素之和 class PrefixSumArray { public: PrefixSumArray(const std::vector<int> arr) { if (!arr.empty()) { prefix_sum.resize(arr.size()); prefix_sum[0] = arr[0]; for (size_t i = 1; i < arr.size(); ++i) { prefix_sum[i] = prefix_sum[i - 1] + arr[i]; } } } // 求区间和: [l, r]之和. int RangeSum(int l, int r) const { if (l == 0) return prefix_sum[r]; // [0, r] return prefix_sum[r] - prefix_sum[l - 1]; // [l, r] } private: std::vector<int> prefix_sum; }; ``` ### 定义方式二 - `prefix_sum[i]` 表示数组 `arr` 中**区间 `[0, i)` 的前缀和**(**前`i` 项**元素之和) - `prefix_sum` 长度为 `n + 1`; - 构建前缀和数组: - `prefix[0] = 0`; - `prefix[i + 1] = prefix[i] + arr[i]`,对 `i = [0, n-1]` ; - 求区间和: - `[l, r]` 的区间和为 `prefix_sum[r + 1] - prefix_sum[l]` > [!NOTE] 优点:`prefix_sum[0] == 0` 相当于哨兵 > > - 统一了初始化逻辑:对 `i` 从`[0, n-1]` 有 `prefix_sum[i + 1] = prefix_sum[i] + nums[i]`; > - 统一了求 "区间和" 的逻辑,对于区间 `[l, r]` 无需分类讨论 `l==0`的情况。 ```cpp // 前缀和数组 // 定义二: `prfixe_sum[i]` 表示下标区间 `[0, i)` 的前 `i` 项元素之和 class PrefixSumArray2 { public: // 构建前缀和数组 PrefixSumArray2(const std::vector<int> arr) { if (!arr.empty()) { prefix_sum.resize(arr.size() + 1); // n + 1 prefix_sum[0] = 0; // 首项为0 for (size_t i = 0; i < arr.size(); ++i) { prefix_sum[i + 1] = prefix_sum[i] + arr[i]; } } } // 返回区间[l, r]之和. int RangeSum(int l, int r) const { return prefix_sum[r+1] - prefix_sum[l - 1]; } private: std::vector<int> prefix_sum; }; ``` <br><br> ## 前缀和数组的应用总结 1. **计算前缀和** - **前 i 项元素之和**; - **最大/最小 k 项元素之和**(为有序数组构建前缀和数组) 2. **计算区间和** 3. 计算数轴上**一组坐标点==两两之间距离==的==总和==** $O(n)$ 4. ...... > [!summary] "前缀和" 的应用 > > "**前缀和**"(更确切的说,某种前缀状态) 的应用主要分为两类: > > - 目标区间范围已知时,要求**查询指定区间的状态**; > - 目标区间范围未知时,要求**找出满足条件的目标区间**; > > 对于上述两类题目,部分场景下还需要先进行预处理:将一些**特定的区间状态**需要先**转换为明确的 "==位置标记 (`-1/0/1`)==" 进行==建模==**,从而能够通过 "**==区间和==**" 来对**区间状态**进行表达。 > ^zaj9km <br><br> ## 前缀和数组的基本应用——利用大小前缀和之差计算 "区间和" 区间 `[l, r]` 的 **==区间和==** = **大前缀区间`[0, r]` 与小前缀区间 `[0, l]` 的==前缀和之差==** 由此,通过预先处理得到一个 "**前缀和数组/累加数组**",可实现**对任意 "==区间和==" 的 $O(1)$ 查询**。 #### 利用前缀和求"环形数组"上的区间和 求**环形数组**上的区间和 `[l, r]`: 将**区间边界下标`l`、`r`** 映射为**原数组上的有效下标 `l_ori`、`r_ori`**,有两种可能情况: 1. 若 `l_ori <= r_ori`: - 区间 `[l, r]` -> 原数组上 **==单段==区间**: `[l_ori, r_ori]`; 2. 若 `l_ori > r_ori`: - 区间 `[l, r]` -> 原数组上 **==首尾两段==区间**: `[0, r_ori]` & `[l_ori, n) 因此,分类讨论,如下所示: ```cpp // 返回环形数组上的区间[l, r]之和. int RangeSumInCircularArray(vector<int>& prefix_sum, int l, int r) { int n = prefix_sum.size() - 1; // 将边界下标映射为原数组中的下标 int l_ori = (l%n+n)%n; int r_ori = (r%n+n)%n; if (l_ori <= r_ori) { return prefix_sum[r_ori+1] - prefix_sum[l_ori]; } else { return prefix_sum[r_ori+1] + prefix_sum[n] - prefix_sum[l_ori]; } } ``` #### 利用前缀和数组求区间中的字符频次 ```cpp // 构造前缀和数组, 记录字符频次. vector<vector<int>> prefix_sum(26, vector<int>(n + 1, 0)); for (int i = 0; i < n; ++i) { for (int j = 0; j < 26; ++j) { prefix_sum[j][i + 1] = prefix_sum[j][i]; } ++prefix_sum[s[i] - 'a'][i + 1]; } ``` <br><br> ## 前缀和数组的扩展应用 ### 前缀和 & 二分查找 ##### (1)为数组中每个元素获取其左右侧"最远"的 "更大/更小元素" - 构建**前/后缀状态数组**,记录 **==前/后缀序列中的最小值 or 最大值==**; - 为数组中每项元素`arr[i]`,在**前/后缀数组上==二分查找==**。 | 查找目标 | 所需构建的前后缀数组 | 二分查找目标 | | -------------------- | ----------- | ------------------- | | 左侧 "最远" 的 "更**小**元素" | 前缀**最小值**数组 | `<arr[i]` 的**首**项元素 | | 左侧 "最远" 的 "更**大**元素" | 前缀**最大值**数组 | `>arr[i]` 的**首**项元素 | | 右侧 "最远" 的 "更**小**元素" | 后缀**最小值**数组 | `<arr[i]` 的**尾**项元素 | | 右侧 "最远" 的 "更**大**元素" | 后缀**最大值**数组 | `>arr[i]` 的**尾**项元素 | ![[Excalidraw-Solution/前缀和与差分数组_2.excalidraw.svg|455]] %%[[Excalidraw-Solution/前缀和与差分数组_2.excalidraw.md|🖋 Edit in Excalidraw]]%% <br><br> # 一维后缀和数组 后缀和数组只存在**一种有效定义**:**`suffix[i]` 表示区间 `[i, n-1]` 的后缀和**。 区分后缀和数组长度设置为 `n` 与 `n + 1` 两种情况: ##### 后缀和数组长度为 n ```cpp // 构建后缀和数组 vector<int> suffix(n); suffix[n-1] = arr[n-1]; for (int i = n - 2; i >= 0; --i) { suffix[i] = suffix[i + 1] + arr[i]; } // 利用后缀和计算[l, r]区间和 void range(int l, int r, vector<int>& suffix) { if (r == n) return suffix[l]; return suffix[l] - suffix[r + 1]; } ``` ##### 后缀和数组长度为 n+1 ```cpp // 构建后缀和数组 vector<int> suffix(n + 1); suffix[n] = 0; // 最后一项初始化为0, 用作哨兵. for (int i = n - 1; i >= 0; --i) { suffix[i] = suffix[i-1] + arr[i]; } // 利用后缀和计算[l, r]区间和 void range(int l, int r, vector<int>& suffix) { return suffix[l] - suffix[r + 1]; // 无需再讨论r == n-1的情况. } ``` <br><br> # 前后缀数组不同定义方式的应用差异示例 ##### 示例一 参见 [[99-题解/leetcode/lc-2865 美丽塔 I#思路二:应用两次单调栈|lc-2865 美丽塔 I#思路二:应用两次单调栈]] 前后缀数组长度定义为 `n + 1`: ```cpp // 思路二: // 写法一: 前缀和数组长度定义为n+1 class Solution { public: long long maximumSumOfHeights(vector<int>& heights) { int n = heights.size(); vector<long long> suffix(n + 1, 0); vector<long long> prefix(n + 1, 0); stack<int> st; for (int i = 0; i < n; ++i) { while (!st.empty() && heights[st.top()] >= heights[i]) { st.pop(); } int l = st.empty() ? -1 : st.top(); // [0, i]从左到右递增的前缀高度和 = [0, l]的前缀高度和, 再加上区间(l, i]都以heights[i]为高度. prefix[i + 1] = prefix[l + 1] + 1LL * (i - l) * heights[i]; st.push(i); } st = stack<int>(); for (int i = n - 1; i >= 0; --i) { while (!st.empty() && heights[i] <= heights[st.top()]) { st.pop(); } // [i, n-1]从右到左递增的后缀高度和 = [r, n-1]的后缀高度和 int r = st.empty() ? n : st.top(); suffix[i] = suffix[r] + 1LL * (r - i) * heights[i]; st.push(i); } long long max_sum = 0; for (int i = 0; i < n; ++i) { // [0, i]的前缀以及[i, n-1]的后缀, 重复计入了一个height[i], 需要减去 long long sum = prefix[i + 1] + suffix[i] - heights[i]; max_sum = max(max_sum, sum); } return max_sum; } }; ``` 前后缀数组长度定义为 `n`: ```cpp // 思路二: // 写法二: 前后缀数组长度定义为n class Solution { public: long long maximumSumOfHeights(vector<int>& heights) { int n = heights.size(); vector<long long> suffix(n, 0); vector<long long> prefix(n, 0); stack<int> st; for (int i = 0; i < n; ++i) { while (!st.empty() && heights[st.top()] >= heights[i]) { st.pop(); } // [0, i]从左到右递增的前缀高度和 = [0, l]的前缀高度和, 再加上区间(l, i]都以heights[i]为高度. if (st.empty()) { prefix[i] = 1LL * (i + 1) * heights[i]; } else { int l = st.top(); prefix[i] = prefix[l] + 1LL * (i - l) * heights[i]; } st.push(i); } st = stack<int>(); for (int i = n - 1; i >= 0; --i) { while (!st.empty() && heights[i] <= heights[st.top()]) { st.pop(); } // [i, n-1]从右到左递增的后缀高度和 = [r, n-1]的后缀高度和 if (st.empty()) { suffix[i] = 1LL * (n - i) * heights[i]; } else { int r = st.top(); suffix[i] = suffix[r] + 1LL * (r - i) * heights[i]; } st.push(i); } long long max_sum = 0; for (int i = 0; i < n; ++i) { // [0, i]的前缀以及[i, n-1]的后缀, 重复计入了一个height[i], 需要减去 max_sum = max(max_sum, prefix[i] + suffix[i] - heights[i]); } return max_sum; } }; ``` <br><br><br> # 二维前缀和矩阵 适用场景:矩阵中 **==矩形区域求和==** ### 构建前缀和矩阵 ![[Excalidraw-Solution/前缀和与差分数组_0.excalidraw.svg|500]] %%[[Excalidraw-Solution/前缀和与差分数组_0.excalidraw.md|🖋 Edit in Excalidraw]]%% ![[_attachment/00-数据结构与算法笔记/专题总结/前缀和与差分数组.assets/IMG-前缀和与差分数组-C5586C7A24C38736BB3CB3089AE8906B.png|425]] 定义 `prefix_sum[i+1][j+1]` **表示以 `[0, 0]` 为左上角、`[i, j]` 为右下角的矩阵的区域和**。 ### 计算区间和 给定区间**左上角座标 `(r1, c1)`**、**右下角坐标 `(r2, c2)`**,计算**区域和**。 ![[Excalidraw-Solution/前缀和与差分数组_3.excalidraw.svg|745]] %% [[Excalidraw-Solution/前缀和与差分数组_3.excalidraw.md|🖋 Edit in Excalidraw]] %% ### 实现 ```cpp class PrefixSumMatrix { public: // 构造函数,计算二维前缀和矩阵 PrefixSumMatrix(const std::vector<std::vector<int>>& matrix) { if (matrix.empty() || matrix[0].empty()) return; int m = matrix.size(), n = matrix[0].size(); prefix_sum.assign(m + 1, std::vector<int>(n + 1, 0)); for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { prefix_sum[i + 1][j + 1] = matrix[i][j] + prefix_sum[i][j+1] + prefix_sum[i+1][j] - prefix_sum[i][j]; } } } // 计算区域[(r1, c1), (r2, c2)]的和,区域由左上角(r1, c1)和右下角(r2, c2)的坐标指定 int regionSum(int r1, int c1, int r2, int c2) const { return prefix_sum[r2+1][c2+1] - prefix_sum[r2+1][c1] - prefix_sum[r1][c2+1] + prefix_sum[r1][c1]; } private: std::vector<std::vector<int>> prefix_sum; }; ``` <br><br><br> # 差分数组 「**前缀和数组**」、「**原数组**」、「**差分数组**」具有下列关系: ![[_attachment/00-数据结构与算法笔记/专题总结/前缀和与差分数组.assets/IMG-前缀和与差分数组-B9FF205BFC9DF4B2AA6E65A05FB5CA17.png|450]] 适用场景:**需要频繁处理大量区间更新操作的场景** **对数组==某区间内所有元素==进行同一加减操作时,可将时间复杂度从 $O(n)$ 降为 $O(1)$,通过维护差分数组来累积多次的差量,最后一次遍历完成对区间内所有元素的更新,得到最终修改后的数组。** ## 定义 ![[_attachment/00-数据结构与算法笔记/专题总结/前缀和与差分数组.assets/IMG-前缀和与差分数组-27965E14FC44359D696284CBD8606BD0.png|575]] ## 构建差分数组 ![[Excalidraw-Solution/前缀和与差分数组_4.excalidraw.svg|433]] %%[[Excalidraw-Solution/前缀和与差分数组_4.excalidraw.md|🖋 Edit in Excalidraw]]%% ## 差分数组相关操作 主要操作包括三种: - 对**原数组指定区间 `[l,r]` 中所有元素进行增减操作** - **重构原数组** - 查询原数组中位置 `i` 的元素 #### 指定区间的批量增减操作 对**原数组指定区间 `[l,r]` 中所有元素进行批量增减操作: - **全部增加 `k`**:等价于对差分数组 `diff[l] += k;` 、`diff[r+1] -= k`; - **全部减少 `k`**:等价于对差分数组 `diff[l] -= k;` 、`diff[r+1] += k;` > [!caution] 使用差分数组来**==累计对 "原数组" 的增量==**时,**差分数组是对 "==增量值==" 的差分**,而不是对原数组的差分。 ![[Excalidraw-Solution/前缀和与差分数组_5.excalidraw.svg|419]] %%[[Excalidraw-Solution/前缀和与差分数组_5.excalidraw.md|🖋 Edit in Excalidraw]]%% ![[Excalidraw-Solution/前缀和与差分数组_6.excalidraw.svg|507]] %%[[Excalidraw-Solution/前缀和与差分数组_6.excalidraw.md|🖋 Edit in Excalidraw]]%% #### 重构原数组 ![[Excalidraw-Solution/前缀和与差分数组_7.excalidraw.svg|410]] %%[[Excalidraw-Solution/前缀和与差分数组_7.excalidraw.md|🖋 Edit in Excalidraw]]%% ## 差分数组实现 ```cpp class DiffArray { private: std::vector<int> diff; public: // 构建差分数组 DiffArray(std::vector<int>& array) { if (array.empty()) return; int n = array.size(); diff.resize(n); diff[0] = array[0]; for (int i = 1; i < n; ++i) { diff[i] = array[i] - array[i-1]; } } // 查询原数组中位置i的元素值 int Query(int i) { if (i >= diff.size()) throw std::runtime_error("Out of Bounds"); int x = 0; for (int j = 0; j <= i; ++j) { x += diff[j]; } return x; } // 对原数组中区间[l, r]内的元素进行增量操作 void Increment(int l, int r, int val) { diff[l] += val; // 在diff[l]增加val if (r + 1 < diff.size()) { // 在diff[r+1]减少val diff[r + 1] -= val; } } // 重构原数组 std::vector<int> Recover() { std::vector<int> origin(diff.size(), 0); origin[0] = diff[0]; for (int i = 1; i < diff.size(); ++i) { origin[i] = origin[i-1] + diff[i]; } return origin; } }; ``` ## 典型应用 1. **给定==差分数组==,通过求==前缀和==来==复原==原数组的值** 2. 利用差分数组来**累计区间增量**,得到 "**增量值的差分**",再对该**差分数组求前缀和**,得到**各个位置的总增量**。 3. 利用差分数组**判断区间覆盖情况**,求解 "**最大重叠区间数量**"。 <br><br><br> # 参考资料 # Footnotes