一、什么是前缀和?
前缀和是一种用于快速计算数组中任意区间和的技巧。通过预处理数组,构造一个前缀和数组 prefix[i]
,使得 prefix[i]
表示原数组从第 0 个元素到第 i 个元素的累加和。
公式为:
- 前缀和数组构造:
prefix[i] = prefix[i-1] + nums[i]
(prefix[0] = nums[0]
) - 区间和计算:
sum(i, j) = prefix[j] - prefix[i-1]
(注意边界条件)
二、前缀和的题目类型
前缀和的题目可以分为以下几类:
1. 区间和问题
- 特点:给定一个数组,要求快速计算任意区间
[i, j]
的和。 - 典型题目:
2. 满足条件的子数组/子区间问题
- 特点:统计满足某种条件的子数组数量或长度。
- 典型题目:
3. 二维前缀和
- 特点:在二维矩阵中快速计算任意子矩形的元素和。
- 典型题目:
4. 差分数组(前缀和的逆向应用)
- 特点:通过差分数组快速处理区间修改问题。
- 典型题目:
三、解题思路与技巧
1. 如何快速识别前缀和题目?
- 关键词:
- “区间和”
- “子数组”
- “满足条件的子区间”
- “快速计算”
- 题目特征:
- 需要频繁计算数组的某些区间和。
- 需要统计满足某种条件的子数组数量。
- 涉及二维矩阵的子矩形和。
2. 解题步骤
- 明确问题类型:是区间和问题,还是统计满足条件的子数组问题?
- 构造前缀和数组:根据题目需求,构造一维或二维前缀和数组。
- 利用前缀和公式简化计算:通过
prefix[j] - prefix[i-1]
或类似公式快速计算区间和。 - 优化复杂度:结合哈希表、滑动窗口等技巧,进一步优化时间复杂度。
3. 常见优化技巧
- 哈希表优化:
在统计满足条件的子数组问题中,利用哈希表记录前缀和的出现次数,可以将时间复杂度从 O(n^2) 降低到 O(n)。
示例:和为 K 的子数组
。 - 二维前缀和公式:
在二维矩阵中,快速计算子矩形和的公式为:
sum(x1, y1, x2, y2) = prefix[x2][y2] - prefix[x1-1][y2] - prefix[x2][y1-1] + prefix[x1-1][y1-1]
。
四、题目练习路径
1. 入门题目
- LeetCode 303. 区域和检索 - 数组不可变
目标:理解前缀和的基本构造与区间和计算。 - LeetCode 560. 和为 K 的子数组
目标:学会结合前缀和与哈希表优化。
2. 进阶题目
- LeetCode 974. 和可被 K 整除的子数组
目标:掌握前缀和与取模运算的结合。 - LeetCode 304. 二维区域和检索 - 矩阵不可变
目标:学习二维前缀和的构造与应用。
3. 高阶题目
- LeetCode 1109. 航班预订统计
目标:理解差分数组的应用。 - LeetCode 1094. 拼车
目标:结合差分数组与区间修改问题。
五、练习方法与提升技巧
-
分阶段练习
- 第一阶段:从简单的区间和问题入手,熟悉前缀和的构造与基本应用。
- 第二阶段:练习结合哈希表优化的题目,提升复杂问题的解决能力。
- 第三阶段:挑战二维前缀和与差分数组,扩展思维广度。
-
总结题型与模板
- 每做一道题目,记录其题型、解题思路与代码模板,形成自己的“前缀和题库”。
-
多思考优化方向
- 在解决问题后,思考是否可以进一步优化时间或空间复杂度。
六、前缀和的代码模板
1. 一维前缀和模板
public class PrefixSum1D {
public static void main(String[] args) {
int[] nums = {1, 2, 3, 4, 5};
int n = nums.length;
// 构造前缀和数组
int[] prefix = new int[n + 1];
for (int i = 1; i <= n; i++) {
prefix[i] = prefix[i - 1] + nums[i - 1];
}
// 区间 [i, j] 的和
int i = 1, j = 3; // 对应原数组下标区间 [1, 3]
int result = prefix[j + 1] - prefix[i];
System.out.println("区间和: " + result); // 输出结果
}
}
2. 哈希表优化(和为 K 的子数组)
import java.util.HashMap;
import java.util.Map;
public class SubarraySumEqualsK {
public static void main(String[] args) {
int[] nums = {1, 1, 1};
int k = 2;
int prefixSum = 0;
int count = 0;
// Map<前缀和, 该前缀和出现的次数>
Map<Integer, Integer> prefixMap = new HashMap<>();
// 初始化:前缀和为 0 出现 1 次
prefixMap.put(0, 1);
for (int num : nums) {
prefixSum += num;
// 若 prefixSum - k 在 Map 中出现过,则找到若干符合条件的子数组
if (prefixMap.containsKey(prefixSum - k)) {
count += prefixMap.get(prefixSum - k);
}
// 维护当前 prefixSum 出现次数
prefixMap.put(prefixSum, prefixMap.getOrDefault(prefixSum, 0) + 1);
}
System.out.println("和为 " + k + " 的子数组个数: " + count);
}
}
3. 二维前缀和模板
public class PrefixSum2D {
public static void main(String[] args) {
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
int rows = matrix.length;
int cols = matrix[0].length;
// 构造二维前缀和数组
int[][] prefix2D = new int[rows + 1][cols + 1];
for (int i = 1; i <= rows; i++) {
for (int j = 1; j <= cols; j++) {
prefix2D[i][j] = matrix[i - 1][j - 1]
+ prefix2D[i - 1][j]
+ prefix2D[i][j - 1]
- prefix2D[i - 1][j - 1];
}
}
// 子矩形和计算,假设子矩形左上角 (x1, y1),右下角 (x2, y2)
int x1 = 1, y1 = 1; // 注意:基于数组下标从 0 开始时,这里是第二行第二列
int x2 = 2, y2 = 2; // 第三行第三列
// 计算子矩形 (x1, y1) 到 (x2, y2) 的元素和
int subRectSum = prefix2D[x2 + 1][y2 + 1]
- prefix2D[x1][y2 + 1]
- prefix2D[x2 + 1][y1]
+ prefix2D[x1][y1];
System.out.println("子矩形和: " + subRectSum);
}
}
通过以上方法与练习路径,你可以从入门到精通前缀和,逐步掌握其在算法题中的应用。坚持练习,祝你早日成为算法高手!