2025.11.14 力扣每日一题
2536.子矩阵元素加1
差分数组这个是真的有点难理解,琢磨好长时间。。。
#include <vector>
using namespace std;class Solution {
public:vector<vector<int>> rangeAddQueries(int n, vector<vector<int>>& queries) {// 1. 初始化二维差分数组// 大小为 (n+2)×(n+2),额外的两行两列是为了避免边界判断(防止数组越界)vector<vector<int>> diff(n + 2, vector<int>(n + 2, 0));// 2. 处理所有查询,在差分数组中标记子矩阵加1的操作for (auto & q : queries) {// 提取查询的边界:左上角(r1,c1),右下角(r2,c2)int r1 = q[0], c1 = q[1], r2 = q[2], c2 = q[3];// 核心:对差分数组的四个角落进行标记,实现"子矩阵加1"的差分记录// 标记1:子矩阵左上角的右下角位置 +1(表示从这里开始加1)diff[r1 + 1][c1 + 1]++;// 标记2:子矩阵右上角的右侧位置 -1(表示列方向结束加1)diff[r1 + 1][c2 + 2]--;// 标记3:子矩阵左下角的下方位置 -1(表示行方向结束加1)diff[r2 + 2][c1 + 1]--;// 标记4:子矩阵右下角的右下方位置 +1(抵消重复减去的部分,保证区域精确性)diff[r2 + 2][c2 + 2]++;}// 3. 计算二维前缀和,将差分数组转化为最终结果矩阵vector<vector<int>> ans(n, vector<int>(n, 0)); // 存储最终结果的n×n矩阵// 遍历原矩阵的每个位置(i,j)(0-based)for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {// 计算差分数组在(i+1,j+1)位置的二维前缀和// 公式:当前值 += 左方前缀和 + 上方前缀和 - 左上角重叠区域前缀和// 这一步会将之前的"角落标记"扩散到整个矩阵,得到每个位置的总加1次数diff[i + 1][j + 1] += diff[i + 1][j] + diff[i][j + 1] - diff[i][j];// 将计算结果存入答案矩阵(差分数组的(i+1,j+1)对应原矩阵的(i,j))ans[i][j] = diff[i + 1][j + 1];}}// 返回最终矩阵return ans;}
};
差分数组(Difference Array)知识点总结
1. 定义与核心思想
- 定义:差分数组是一种用于高效处理区间更新的数据结构,通过记录 “相邻元素的差值” 来简化多次区间增减操作。
- 核心思想:将 “对区间
[l, r]内所有元素加 / 减val” 的操作,转化为对差分数组的两个端点的修改,最终通过前缀和还原出原数组。
2. 一维差分数组
(1)构造方法
对于原数组 arr(长度为 n),差分数组 diff 定义为:
diff[0] = arr[0]diff[i] = arr[i] - arr[i-1](i ≥ 1)
(2)区间更新操作
若要对区间 [l, r] 内所有元素加 val,只需修改差分数组的两个位置:
diff[l] += val; // 从 l 开始的元素都加 val
if (r + 1 < n) {diff[r + 1] -= val; // 从 r+1 开始的元素抵消加 val(恢复原状)
}
(3)还原原数组
对差分数组 diff 计算前缀和,即可得到更新后的原数组:
arr[0] = diff[0];
for (int i = 1; i < n; i++) {arr[i] = arr[i-1] + diff[i];
}
3. 二维差分数组
(1)适用场景
用于二维矩阵的子矩阵区间更新(如对 (r1,c1)~(r2,c2) 内所有元素加 val)。
(2)区间更新操作
对二维差分数组 diff 的四个角落进行标记,即可实现子矩阵更新:
// 对以 (r1,c1) 为左上角、(r2,c2) 为右下角的子矩阵加 val
diff[r1][c1] += val;
diff[r1][c2 + 1] -= val; // 列方向结束标记
diff[r2 + 1][c1] -= val; // 行方向结束标记
diff[r2 + 1][c2 + 1] += val; // 抵消重复标记
- 注:实际实现中通常将
diff大小设为(n+2)×(n+2),避免边界越界判断。
(3)还原原矩阵
对二维差分数组 diff 计算二维前缀和,即可得到更新后的矩阵:
// 二维前缀和递推公式
diff[i][j] += diff[i-1][j] + diff[i][j-1] - diff[i-1][j-1];
// 此时 diff[i][j] 即为原矩阵 (i,j) 位置的最终值
4. 优势与适用场景
- 优势:将多次区间更新的时间复杂度从
O(k·m)(k为操作次数,m为区间长度)优化为O(k + n)(一维)或O(k + n²)(二维),大幅提升效率。 - 适用场景:
- 频繁对连续区间 / 子矩阵进行加 / 减操作。
- 最终需要获取每个元素的具体值(而非仅统计区间和)。
5. 与前缀和的关系
- 互为逆操作:
- 前缀和:将原数组转化为 “前缀和数组”,用于快速计算区间和。
- 差分数组:将原数组转化为 “差分标记”,用于快速执行区间更新,再通过前缀和还原。
- 流程:原数组 → 差分数组(记录更新) → 前缀和 → 更新后的原数组。
6. 典型例题
- 一维:LeetCode 1109. 航班预订统计(区间增减后求最终座位数)。
- 二维:LeetCode 2536. 子矩阵元素加 1(多次子矩阵加 1 后求最终矩阵)。
总结
差分数组是处理 “区间更新” 问题的高效工具,核心在于通过 “端点标记” 替代 “全区间修改”,再利用前缀和还原结果。掌握一维和二维差分数组的标记规则和还原方法,可快速解决各类区间更新问题。
前缀和(Prefix Sum)知识点总结
前缀和是一种用于快速计算数组 / 矩阵区间和的数据结构,核心是通过 “预处理前缀和数组”,将任意区间和的查询时间复杂度从 O(n) 优化到 O(1),是算法中处理 “区间求和” 问题的基础工具。
一、核心定义与核心思想
1. 定义
前缀和数组的每个元素,代表原数组从起始位置到当前位置的所有元素之和。
- 一维场景:前缀和数组
preSum[i]= 原数组arr[0] + arr[1] + ... + arr[i-1](通常前缀和数组比原数组长 1,preSum[0] = 0作为哨兵)。 - 二维场景:前缀和数组
preSum[i][j]= 原矩阵mat[0][0] + ... + mat[i-1][j-1](左上角到(i-1,j-1)的所有元素和)。
2. 核心思想
通过预处理一次前缀和数组,将 “多次区间和查询” 转化为 “前缀和数组的差值计算”,避免重复遍历原数组,提升查询效率。
二、一维前缀和(最基础场景)
1. 预处理前缀和数组
给定原数组 arr(长度为 n),构造前缀和数组 preSum(长度为 n+1):
vector<int> preSum(n + 1, 0);
for (int i = 0; i < n; i++) {preSum[i + 1] = preSum[i] + arr[i]; // 递推公式:前i+1个元素和 = 前i个元素和 + 当前元素
}
2. 快速查询区间和
查询原数组区间 [l, r](0-based,包含 l 和 r)的和:
int sum = preSum[r + 1] - preSum[l]; // 核心公式:区间和 = 前r+1个和 - 前l个和
示例
- 原数组
arr = [1, 2, 3, 4, 5] - 前缀和数组
preSum = [0, 1, 3, 6, 10, 15] - 查询
[1, 3](元素 2+3+4):preSum[4] - preSum[1] = 10 - 1 = 9,结果正确。
三、二维前缀和(矩阵区间求和)
1. 适用场景
快速计算二维矩阵中 “任意子矩阵的和”(如左上角 (x1,y1) 到右下角 (x2,y2) 的元素和)。
2. 预处理前缀和数组
给定原矩阵 mat(n×m 大小),构造前缀和数组 preSum((n+1)×(m+1) 大小):
vector<vector<int>> preSum(n + 1, vector<int>(m + 1, 0));
for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {// 递推公式:利用容斥原理,避免重复计算重叠区域preSum[i + 1][j + 1] = preSum[i][j + 1] + preSum[i + 1][j] - preSum[i][j] + mat[i][j];}
}
- 拆解公式:
preSum[i][j+1]:上方区域的前缀和preSum[i+1][j]:左方区域的前缀和preSum[i][j]:上方 + 左方的重叠区域(被重复加了两次,需减去)mat[i][j]:当前位置的元素值
3. 快速查询子矩阵和
查询子矩阵 (x1,y1) ~ (x2,y2)(0-based,包含边界)的和:
int sum = preSum[x2 + 1][y2 + 1] - preSum[x1][y2 + 1] - preSum[x2 + 1][y1] + preSum[x1][y1];
- 拆解公式:
preSum[x2+1][y2+1]:整个矩阵到(x2,y2)的前缀和- 减去
preSum[x1][y2+1]:上方不需要的区域和 - 减去
preSum[x2+1][y1]:左方不需要的区域和 - 加上
preSum[x1][y1]:被重复减去的重叠区域和(需补回)
示例
- 原矩阵
mat = [[1,2],[3,4]] - 前缀和数组
preSum = [[0,0,0],[0,1,3],[0,4,10]] - 查询子矩阵
(0,0)~(1,1)(全矩阵和):preSum[2][2] - preSum[0][2] - preSum[2][0] + preSum[0][0] = 10 - 0 - 0 + 0 = 10,结果正确(1+2+3+4=10)。
四、核心性质与优势
- 时间复杂度:预处理
O(n)(一维)或O(n×m)(二维),单次查询O(1),多次查询时优势极明显。 - 空间复杂度:
O(n)(一维)或O(n×m)(二维),需额外存储前缀和数组。 - 无修改场景:前缀和适用于 “原数组 / 矩阵不修改,仅查询区间和” 的场景;若有频繁修改,需用树状数组或线段树。
五、典型例题
- 一维:LeetCode 303. 区域和检索 - 数组不可变(基础区间和查询)。
- 二维:LeetCode 304. 二维区域和检索 - 矩阵不可变(基础子矩阵和查询)。
- 拓展:LeetCode 560. 和为 K 的子数组(前缀和 + 哈希表,统计满足条件的子数组个数)。
六、与差分数组的关系
- 互为逆操作:
- 前缀和:原数组 → 前缀和数组(用于快速查询区间和)。
- 差分数组:原数组 → 差分数组(用于快速执行区间更新),再通过前缀和还原原数组。
- 组合使用:区间更新 + 区间查询 场景(先差分更新,再前缀和查询)。
总结
前缀和的核心价值是 “预处理换快速查询”,是处理 “静态区间和” 问题的最优工具。掌握一维和二维前缀和的构造公式与查询公式,可快速解决各类区间求和问题,是算法入门的必学知识点。
前缀和核心公式速查表
这份速查表涵盖一维、二维前缀和的核心公式、代码模板及典型例题,方便快速查阅和直接套用。
一、一维前缀和(核心公式 + 模板)
1. 核心公式
- 预处理前缀和数组:
preSum[i+1] = preSum[i] + arr[i](preSum[0] = 0,哨兵简化计算) - 区间和查询(原数组
[l, r],0-based,含边界):sum = preSum[r+1] - preSum[l]
2. 代码模板
// 1. 预处理前缀和
vector<int> getPrefixSum(vector<int>& arr) {int n = arr.size();vector<int> preSum(n + 1, 0);for (int i = 0; i < n; i++) {preSum[i + 1] = preSum[i] + arr[i];}return preSum;
}// 2. 查询区间 [l, r] 的和
int queryRangeSum(vector<int>& preSum, int l, int r) {return preSum[r + 1] - preSum[l];
}
3. 典型例题(LeetCode 303. 区域和检索 - 数组不可变)
class NumArray {
private:vector<int> preSum;
public:NumArray(vector<int>& nums) {int n = nums.size();preSum.resize(n + 1, 0);for (int i = 0; i < n; i++) {preSum[i + 1] = preSum[i] + nums[i];}}int sumRange(int left, int right) {return preSum[right + 1] - preSum[left];}
};
二、二维前缀和(核心公式 + 模板)
1. 核心公式
- 预处理前缀和数组:
preSum[i+1][j+1] = preSum[i][j+1] + preSum[i+1][j] - preSum[i][j] + mat[i][j](preSum[0][*] = preSum[*][0] = 0) - 子矩阵和查询(原矩阵
(x1,y1)~(x2,y2),0-based,含边界):sum = preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1]
2. 代码模板
// 1. 预处理前缀和
vector<vector<int>> get2DPrefixSum(vector<vector<int>>& mat) {int n = mat.size(), m = mat[0].size();vector<vector<int>> preSum(n + 1, vector<int>(m + 1, 0));for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {preSum[i + 1][j + 1] = preSum[i][j + 1] + preSum[i + 1][j] - preSum[i][j] + mat[i][j];}}return preSum;
}// 2. 查询子矩阵 (x1,y1)~(x2,y2) 的和
int query2DRangeSum(vector<vector<int>>& preSum, int x1, int y1, int x2, int y2) {return preSum[x2 + 1][y2 + 1] - preSum[x1][y2 + 1] - preSum[x2 + 1][y1] + preSum[x1][y1];
}
3. 典型例题(LeetCode 304. 二维区域和检索 - 矩阵不可变)
class NumMatrix {
private:vector<vector<int>> preSum;
public:NumMatrix(vector<vector<int>>& matrix) {int n = matrix.size(), m = matrix[0].size();preSum.resize(n + 1, vector<int>(m + 1, 0));for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {preSum[i + 1][j + 1] = preSum[i][j + 1] + preSum[i + 1][j] - preSum[i][j] + matrix[i][j];}}}int sumRegion(int row1, int col1, int row2, int col2) {return preSum[row2 + 1][col2 + 1] - preSum[row1][col2 + 1] - preSum[row2 + 1][col1] + preSum[row1][col1];}
};
三、拓展场景(前缀和 + 哈希表)
核心公式
- 前缀和差值:
preSum[i] - preSum[j] = k→ 子数组[j+1, i]的和为k - 哈希表存储:
map[preSum_val] = 出现次数,统计满足条件的子数组个数
典型例题(LeetCode 560. 和为 K 的子数组)
class Solution {
public:int subarraySum(vector<int>& nums, int k) {unordered_map<int, int> mp;mp[0] = 1; // 初始化:前缀和为0出现1次int preSum = 0, count = 0;for (int num : nums) {preSum += num;// 查找是否存在 preSum - k,存在则累加次数if (mp.count(preSum - k)) {count += mp[preSum - k];}mp[preSum]++; // 记录当前前缀和出现次数}return count;}
};
四、关键注意点
- 下标统一:前缀和数组比原数组 / 矩阵多一行一列(哨兵),避免边界判断。
- 容斥原理:二维前缀和的预处理和查询均需用到容斥,避免重复计算或漏算。
- 适用场景:仅适用于 “原数据不修改,仅查询区间和” 的静态场景。
其余后面再做。。。今天先到这儿
