征服区间操作的核心技术!今日深入解析前缀和与差分数组的底层原理与实战应用,覆盖子数组和统计、区间批量更新等高频场景,彻底掌握O(1)复杂度查询与更新的优化艺术。
一、前缀和核心思想
前缀和(Prefix Sum) 是一种通过预处理数组实现快速区间查询的技术,核心特性:
预处理:构建前缀和数组
prefix
,其中prefix[i]
表示原数组前i
项的和快速查询:区间和计算转化为两次前缀和相减(
sum[l, r] = prefix[r] - prefix[l-1]
)扩展性:支持多维扩展(如二维矩阵区域和)
适用场景:
-
频繁查询区间和/区间统计量
-
多维空间区域和计算
-
结合哈希表优化特定和问题
二、前缀和模板与实现
一维前缀和(LeetCode 303)
class NumArray {
private:
vector<int> prefix;
public:
NumArray(vector<int>& nums) {
prefix.resize(nums.size() + 1, 0);
for (int i = 0; i < nums.size(); ++i) {
prefix[i+1] = prefix[i] + nums[i];
}
}
int sumRange(int left, int right) {
return prefix[right+1] - prefix[left];
}
};
二维前缀和(LeetCode 304)、
class NumMatrix {
private:
vector<vector<int>> prefix;
public:
NumMatrix(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
prefix = vector<vector<int>>(m+1, vector<int>(n+1, 0));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
prefix[i+1][j+1] = matrix[i][j] + prefix[i][j+1]
+ prefix[i+1][j] - prefix[i][j];
}
}
}
int sumRegion(int row1, int col1, int row2, int col2) {
return prefix[row2+1][col2+1] - prefix[row1][col2+1]
- prefix[row2+1][col1] + prefix[row1][col1];
}
};
三、差分数组核心思想
差分数组(Difference Array) 是一种优化区间批量更新的技术,核心特性:
差分定义:
diff[i] = arr[i] - arr[i-1]
(diff[0] = arr[0]
)区间更新:对区间
[l, r]
增加val
→diff[l] += val
,diff[r+1] -= val
还原数组:通过前缀和还原更新后的数组
适用场景:
-
频繁区间增减操作
-
航班预订统计、任务调度
-
结合前缀和实现高效更新与查询
四、差分数组模板
区间增减与还原
class Difference {
private:
vector<int> diff;
public:
Difference(vector<int>& nums) {
diff.resize(nums.size());
diff[0] = nums[0];
for (int i = 1; i < nums.size(); ++i) {
diff[i] = nums[i] - nums[i-1];
}
}
void increment(int l, int r, int val) {
diff[l] += val;
if (r + 1 < diff.size()) {
diff[r+1] -= val;
}
}
vector<int> getResult() {
vector<int> res(diff.size());
res[0] = diff[0];
for (int i = 1; i < diff.size(); ++i) {
res[i] = res[i-1] + diff[i];
}
return res;
}
};
五、高频应用场景
场景1:和为K的子数组(LeetCode 560)
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> sumCount;
sumCount[0] = 1; // 处理前缀和恰好为k的情况
int prefix = 0, count = 0;
for (int num : nums) {
prefix += num;
if (sumCount.count(prefix - k)) {
count += sumCount[prefix - k];
}
sumCount[prefix]++;
}
return count;
}
场景2:航班预订统计(LeetCode 1109)
vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {
vector<int> diff(n + 1, 0); // 差分数组
for (auto& b : bookings) {
int l = b[0]-1, r = b[1]-1, val = b[2];
diff[l] += val;
diff[r+1] -= val;
}
vector<int> res(n);
res[0] = diff[0];
for (int i = 1; i < n; ++i) {
res[i] = res[i-1] + diff[i];
}
return res;
}
场景3:二维区域和检索(LeetCode 308 扩展)
// 动态更新 + 二维前缀和
class NumMatrix {
private:
vector<vector<int>> matrix;
vector<vector<int>> prefix;
public:
NumMatrix(vector<vector<int>>& mat) {
matrix = mat;
int m = matrix.size(), n = matrix[0].size();
prefix = vector<vector<int>>(m+1, vector<int>(n+1, 0));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
prefix[i+1][j+1] = matrix[i][j] + prefix[i][j+1]
+ prefix[i+1][j] - prefix[i][j];
}
}
}
void update(int row, int col, int val) {
int delta = val - matrix[row][col];
matrix[row][col] = val;
for (int i = row+1; i < prefix.size(); ++i) {
for (int j = col+1; j < prefix[0].size(); ++j) {
prefix[i][j] += delta;
}
}
}
int sumRegion(int row1, int col1, int row2, int col2) {
return prefix[row2+1][col2+1] - prefix[row1][col2+1]
- prefix[row2+1][col1] + prefix[row1][col1];
}
};
六、大厂真题实战
真题1:区间加法(某大厂2023面试)
题目描述:
初始化一个全零数组,执行多个区间加法操作后返回最终数组
差分数组解法:
vector<int> getModifiedArray(int length, vector<vector<int>>& operations) {
vector<int> diff(length + 1, 0);
for (auto& op : operations) {
int l = op[0], r = op[1], val = op[2];
diff[l] += val;
if (r + 1 < length) diff[r+1] -= val;
}
vector<int> res(length);
res[0] = diff[0];
for (int i = 1; i < length; ++i) {
res[i] = res[i-1] + diff[i];
}
return res;
}
真题2:连续子数组和(LeetCode 523)
题目描述:
判断是否存在长度≥2的子数组,其和是k的倍数
前缀和+哈希表解法:
bool checkSubarraySum(vector<int>& nums, int k) {
unordered_map<int, int> remainderMap;
remainderMap[0] = -1; // 处理前缀和本身满足条件的情况
int prefix = 0;
for (int i = 0; i < nums.size(); ++i) {
prefix += nums[i];
int rem = prefix % k;
if (remainderMap.count(rem)) {
if (i - remainderMap[rem] >= 2) return true;
} else {
remainderMap[rem] = i;
}
}
return false;
}
七、常见误区与优化技巧
-
索引偏移错误:前缀和数组长度应为
n+1
,注意原数组与前缀和的索引对应关系 -
哈希表遗漏初始状态:未初始化
sumCount[0] = 1
导致漏解 -
差分数组越界:区间右端点
r+1
可能超出数组范围 -
优化技巧:
-
二维前缀和预处理公式的推导
-
动态更新时的增量优化
-
结合位运算压缩状态
-
LeetCode真题训练: