第 05 站 · 线性结构
前缀和与区间
前缀和 + 哈希计数 时间 O(n) · 空间 O(n)
先做这道
LC 724寻找数组的中心下标卡住再看
用总和与左侧累计和计算右侧和,避免重复区间求和。
要点
- 区间和等于两个前缀和之差:sum(l..r) = pre[r+1] - pre[l];前缀数组多开一位,pre[0] = 0。
- 统计「和为 k 的子数组」时,哈希表先放 {0: 1},代表从下标 0 开始的子数组。
- 多次区间加值用差分数组:diff[l] += v,diff[r+1] -= v,最后求一遍前缀和;合并区间先按左端点排序。
易错点
- 忘了放 {0: 1},会漏掉所有从下标 0 开始的子数组。
- 前缀和很容易超过 32 位整数,C++ 用 long long,Java 用 long。
- 先把当前前缀和存进表再查询,k = 0 时会把空子数组也算进去。
看动画
一步一步播放这个模式在小例子上的执行过程。可以暂停、单步前进或后退,拖动进度条跳到任意一步。
前缀和 · 和为 k 的子数组
数组 1, 2, 1, -1, 2, 1 中有多少个和为 3 的连续子数组?
换个输入
- k
- 3
- 前缀和
- 0
- 要找 pre − k
- —
- 子数组个数
- 0
第一行是数组,第二行是前缀和。子数组 i..j 的和 = pre[j+1] − pre[i],所以和为 3 等价于找之前出现过的 pre − 3。空前缀 0 先记 1 次。
始终成立哈希表里存的是当前位置之前每个前缀和出现的次数。
模板
选一种语言展开。四种写法实现的是同一个模板,每一份都编译运行过,并用测试数据核对了输出。看完用它重写热身题 LC 724。
Python
def count_subarrays(nums: list[int], k: int) -> int:
"""Number of non-empty subarrays whose sum is k (negatives allowed)."""
seen = {0: 1} # prefix sum -> occurrences; the empty prefix counts once
pre = ans = 0
for x in nums:
pre += x
ans += seen.get(pre - k, 0) # sum(j..i) = pre_i - pre_j
seen[pre] = seen.get(pre, 0) + 1
return ansC++
// Number of non-empty subarrays whose sum is k (negatives allowed).
int countSubarrays(const vector<int>& nums, int k) {
unordered_map<long long, int> seen{{0, 1}}; // prefix sum -> occurrences
long long pre = 0;
int ans = 0;
for (int x : nums) {
pre += x;
auto it = seen.find(pre - k); // sum(j..i) = pre_i - pre_j
if (it != seen.end()) ans += it->second;
seen[pre]++;
}
return ans;
}Java
class Solution {
// Number of non-empty subarrays whose sum is k (negatives allowed).
int countSubarrays(int[] nums, int k) {
Map<Long, Integer> seen = new HashMap<>(); // prefix sum -> occurrences
seen.put(0L, 1); // the empty prefix
long pre = 0;
int ans = 0;
for (int x : nums) {
pre += x;
ans += seen.getOrDefault(pre - k, 0); // sum(j..i) = pre_i - pre_j
seen.merge(pre, 1, Integer::sum);
}
return ans;
}
}Go
// countSubarrays returns how many non-empty subarrays sum to k (negatives allowed).
func countSubarrays(nums []int, k int) int {
seen := map[int]int{0: 1} // prefix sum -> occurrences; the empty prefix counts once
pre, ans := 0, 0
for _, x := range nums {
pre += x
ans += seen[pre-k] // sum(j..i) = pre_i - pre_j
seen[pre]++
}
return ans
}常见变体
这一站的题还会用到下面 3 种写法。展开看什么时候换用它,代码同样四种语言都跑过测试。
差分数组:区间加,最后一次前缀和时间 O(n + q) · 空间 O(n)
很多次「给区间 [l, r] 加 v」、最后才要结果:在 diff[l] 加 v、diff[r + 1] 减 v,每次 O(1),最后求一遍前缀和还原数组。LC 1094 拼车、LC 1109 航班预订统计、LC 370 区间加法都是这样。
Python
def range_add(n: int, updates: list[tuple[int, int, int]]) -> list[int]:
"""n zeros, then each (l, r, v) adds v to positions l..r (LC 370)."""
diff = [0] * (n + 1) # diff[i] = a[i] - a[i - 1]; the extra slot takes r + 1 = n
for l, r, v in updates:
diff[l] += v
diff[r + 1] -= v # the change stops after r
res, run = [], 0
for i in range(n):
run += diff[i] # prefix sums of the differences rebuild the array
res.append(run)
return resC++
// n zeros, then each {l, r, v} adds v to positions l..r (LC 370).
vector<long long> rangeAdd(int n, const vector<array<int, 3>>& updates) {
vector<long long> diff(n + 1, 0); // diff[i] = a[i] - a[i - 1]; the extra slot takes r + 1 = n
for (auto [l, r, v] : updates) {
diff[l] += v;
diff[r + 1] -= v; // the change stops after r
}
vector<long long> res(n);
long long run = 0;
for (int i = 0; i < n; i++) res[i] = run += diff[i]; // prefix sums rebuild the array
return res;
}Java
class Solution {
// n zeros, then each {l, r, v} adds v to positions l..r (LC 370).
long[] rangeAdd(int n, int[][] updates) {
long[] diff = new long[n + 1]; // diff[i] = a[i] - a[i - 1]; the extra slot takes r + 1 = n
for (int[] u : updates) {
diff[u[0]] += u[2];
diff[u[1] + 1] -= u[2]; // the change stops after r
}
long[] res = new long[n];
long run = 0;
for (int i = 0; i < n; i++) res[i] = run += diff[i]; // prefix sums rebuild the array
return res;
}
}Go
// rangeAdd starts from n zeros; each {l, r, v} adds v to positions l..r (LC 370).
func rangeAdd(n int, updates [][3]int) []int {
diff := make([]int, n+1) // diff[i] = a[i] - a[i-1]; the extra slot takes r + 1 = n
for _, u := range updates {
diff[u[0]] += u[2]
diff[u[1]+1] -= u[2] // the change stops after r
}
res := make([]int, n)
run := 0
for i := 0; i < n; i++ {
run += diff[i] // prefix sums of the differences rebuild the array
res[i] = run
}
return res
}二维前缀和:容斥求子矩形和预处理 O(rows × cols) · 查询 O(1)
矩阵不变、要反复问子矩形的和:pre[r][c] 存左上角 r × c 块的和,多开一行一列 0 免去边界判断;查询时用四个角容斥,O(1)。LC 304 二维区域和检索、LC 1314 矩阵区域和,LC 1074 在它上面再套一维哈希。
Python
class Matrix2D:
"""Sum of any sub-rectangle in O(1) after O(rows × cols) setup (LC 304)."""
def __init__(self, grid: list[list[int]]):
rows, cols = len(grid), len(grid[0])
# pre[r][c]: sum of the top-left r × c block; row 0 and column 0 stay zero
self.pre = [[0] * (cols + 1) for _ in range(rows + 1)]
for r in range(rows):
for c in range(cols):
self.pre[r + 1][c + 1] = self.pre[r][c + 1] + self.pre[r + 1][c] - self.pre[r][c] + grid[r][c]
def sum(self, r1: int, c1: int, r2: int, c2: int) -> int:
"""Corners (r1, c1) and (r2, c2), both inclusive."""
p = self.pre
return p[r2 + 1][c2 + 1] - p[r1][c2 + 1] - p[r2 + 1][c1] + p[r1][c1]C++
// Sum of any sub-rectangle in O(1) after O(rows × cols) setup (LC 304).
class Matrix2D {
vector<vector<long long>> pre; // pre[r][c]: sum of the top-left r × c block
public:
explicit Matrix2D(const vector<vector<int>>& grid) {
int rows = grid.size(), cols = grid[0].size();
pre.assign(rows + 1, vector<long long>(cols + 1, 0)); // row 0 and column 0 stay zero
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++) pre[r + 1][c + 1] = pre[r][c + 1] + pre[r + 1][c] - pre[r][c] + grid[r][c];
}
// Corners (r1, c1) and (r2, c2), both inclusive.
long long sum(int r1, int c1, int r2, int c2) const {
return pre[r2 + 1][c2 + 1] - pre[r1][c2 + 1] - pre[r2 + 1][c1] + pre[r1][c1];
}
};Java
// Sum of any sub-rectangle in O(1) after O(rows × cols) setup (LC 304).
class Matrix2D {
private final long[][] pre; // pre[r][c]: sum of the top-left r × c block
Matrix2D(int[][] grid) {
int rows = grid.length, cols = grid[0].length;
pre = new long[rows + 1][cols + 1]; // row 0 and column 0 stay zero
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++) pre[r + 1][c + 1] = pre[r][c + 1] + pre[r + 1][c] - pre[r][c] + grid[r][c];
}
// Corners (r1, c1) and (r2, c2), both inclusive.
long sum(int r1, int c1, int r2, int c2) {
return pre[r2 + 1][c2 + 1] - pre[r1][c2 + 1] - pre[r2 + 1][c1] + pre[r1][c1];
}
}Go
// Matrix2D answers the sum of any sub-rectangle in O(1) after O(rows × cols) setup (LC 304).
type Matrix2D struct {
pre [][]int // pre[r][c]: sum of the top-left r × c block
}
func NewMatrix2D(grid [][]int) *Matrix2D {
rows, cols := len(grid), len(grid[0])
pre := make([][]int, rows+1) // row 0 and column 0 stay zero
for r := range pre {
pre[r] = make([]int, cols+1)
}
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
pre[r+1][c+1] = pre[r][c+1] + pre[r+1][c] - pre[r][c] + grid[r][c]
}
}
return &Matrix2D{pre}
}
// Sum takes corners (r1, c1) and (r2, c2), both inclusive.
func (m *Matrix2D) Sum(r1, c1, r2, c2 int) int {
p := m.pre
return p[r2+1][c2+1] - p[r1][c2+1] - p[r2+1][c1] + p[r1][c1]
}按起点排序,合并重叠区间时间 O(n log n) · 空间 O(n)
要把区间并起来(合并、插入、求交、算总覆盖长度):按起点排序后,新区间的起点不超过上一个结果的终点就合并、终点取较大者,否则另起一段。LC 56 合并区间、LC 57 插入区间、LC 986 区间列表的交集都从这里出发。
Python
def merge(intervals: list[list[int]]) -> list[list[int]]:
"""Union of closed intervals, by start; touching ones such as [1, 2] and [2, 3] merge."""
res: list[list[int]] = []
for start, end in sorted(intervals):
if res and start <= res[-1][1]: # overlaps the last merged interval
res[-1][1] = max(res[-1][1], end)
else:
res.append([start, end])
return resC++
// Union of closed intervals, by start; touching ones such as [1, 2] and [2, 3] merge.
vector<pair<int, int>> mergeIntervals(vector<pair<int, int>> intervals) {
sort(intervals.begin(), intervals.end());
vector<pair<int, int>> res;
for (auto [start, end] : intervals) {
if (!res.empty() && start <= res.back().second) // overlaps the last merged interval
res.back().second = max(res.back().second, end);
else
res.push_back({start, end});
}
return res;
}Java
class Solution {
// Union of closed intervals, by start; touching ones such as [1, 2] and [2, 3] merge.
List<int[]> merge(int[][] intervals) {
int[][] sorted = intervals.clone();
Arrays.sort(sorted, Comparator.comparingInt(iv -> iv[0]));
List<int[]> res = new ArrayList<>();
for (int[] iv : sorted) {
int[] last = res.isEmpty() ? null : res.get(res.size() - 1);
if (last != null && iv[0] <= last[1]) last[1] = Math.max(last[1], iv[1]); // overlaps the last merged interval
else res.add(new int[] {iv[0], iv[1]});
}
return res;
}
}Go
import "sort"
// merge returns the union of closed intervals, by start; touching ones such as [1, 2] and [2, 3] merge.
func merge(intervals [][2]int) [][2]int {
sorted := append([][2]int(nil), intervals...)
sort.Slice(sorted, func(i, j int) bool { return sorted[i][0] < sorted[j][0] })
var res [][2]int
for _, iv := range sorted {
if n := len(res); n > 0 && iv[0] <= res[n-1][1] { // overlaps the last merged interval
res[n-1][1] = max(res[n-1][1], iv[1])
} else {
res = append(res, iv)
}
}
return res
}为什么对
- sum(l..r) = pre[r+1] − pre[l],所以以 r 结尾、和为 k 的子数组个数,等于之前出现过多少个等于 pre[r+1] − k 的前缀和。哈希表按前缀和计数,一遍扫描把所有子数组按右端点分组数完,每个恰好算一次。
- {0: 1} 代表空前缀 pre[0],没有它就会漏掉从下标 0 开始的子数组。
- 差分数组是前缀和的逆运算:diff[l] += v、diff[r+1] −= v 之后求一次前缀和,只有 [l, r] 内的位置加上了 v。
30 秒口述稿
面试官让你讲思路时,按做法、为什么对、复杂度的顺序说。中英文各一版,可以对着读几遍。
子数组和可以写成两个前缀和的差,pre[r+1] − pre[l]。所以我从左往右扫,维护当前前缀和,同时用哈希表记录每个前缀和出现过几次;以当前位置结尾、和为 k 的子数组数量,就是之前前缀和等于当前值减 k 的次数。哈希表要先放入 {0: 1} 表示空前缀。这样每个子数组按右端点恰好统计一次,时间 O(n),而且对负数也成立。A subarray sum is the difference of two prefix sums, pre[r + 1] minus pre[l]. So I scan once, keep the running prefix sum, and count in a hash map how often each prefix sum has appeared; the subarrays ending here with sum k are exactly the earlier prefixes equal to the current sum minus k. I seed the map with zero seen once for the empty prefix. Every subarray is counted once at its right end, in O(n) time, and unlike a sliding window it works with negative numbers.自评一下
对照上面的答案给自己打个分。没掌握的题会按 1、3、7、21 天的间隔回来,到期的题在「我的进度 · 到期复习」里。 只存在这台设备上;登录后会合并到你的账号。
小测
5 道概念题和复杂度题。答过的题会和 OA 选择题练习一起按间隔回来复习。 去 OA 选择题练习
作答记录只保存在这台设备的浏览器里;登录后打开OA 选择题练习页,会存进账号。
模板练习
用上面的模板和变体出题:把打乱的行排回原序,把挖掉的边界条件填回去,或者在空白编辑器里整段默写,跑本站的测试并逐行对比。每次「换一题」都会重新出题。
下面几行被打乱了。点一行选中,再点另一行和它交换,排回模板原来的顺序;缩进是提示。
分档练习
本站在 500 路里的 25 道题,按基础、核心、进阶分三档。每题附一句话思路,先自己想,想不出再看。
核心 20
| # | 题目 | 一句话思路 | 难度分 |
|---|---|---|---|
| 560 | 和为 K 的子数组 | 用前缀和频次查找差为k的历史状态,支持负数与重复前缀。 | |
| 525 | 连续数组 | 把零替换为负一,再用最早相同前缀位置求最长平衡区间。 | |
| 523 | 连续的子数组和 | 记录余数首次位置,通过重复余数与距离判断长度至少二的区间。 | |
| 974 | 和可被 K 整除的子数组 | 统计相同前缀余数的组合,并统一负数取模的表示。 | |
| 930 | 和相同的二元子数组 | 用前缀频次统计二进制数组目标和,避免连续零带来的漏计。 | |
| 1248 | 统计「优美子数组」 | 把奇偶性映射为零一值,再统计恰好k个奇数的区间。 | |
| 1423 | 可获得的最大点数 | 把两端选取转化为删除中间固定长度窗口,求总和减最小窗口和。 | |
| 1109 | 航班预订统计 | 用差分记录批量航班区间增量,通过一次前缀恢复最终值。 | |
| 1094 | 拼车 | 按站点累计上下车事件,准确处理同站先下车再上车的容量边界。 | |
| 2381 | 字母移位 II | 用带符号差分批量移动字符,并正确规范化负向循环位移。 | |
| 56 | 合并区间 | 按起点排序合并相交区间,维护当前区间的最远终点。 | |
| 57 | 插入区间 | 在线性扫描中区分新插入区间之前、相交及之后三段。 | |
| 986 | 区间列表的交集 | 双指针求两个有序不交区间列表的交集,每次推进终点较早者。 | |
| 435 | 无重叠区间 | 按结束时间进行区间选择,把最少删除转成最多保留。 | |
| 452 | 用最少数量的箭引爆气球 | 把射箭位置选在最早结束边界,正确处理闭区间端点重合。 | |
| 253 | 会议室 II 🔒 | 用结束时间堆或扫描线计算同时占用的最少会议室数。 | |
| 1288 | 删除被覆盖区间 | 起点升序、同起点终点降序后扫描,准确判断区间覆盖。 | |
| 1314 | 矩阵区域和 | 构建二维前缀和,用四项容斥回答截断边界的矩形和。 | |
| 2055 | 蜡烛之间的盘子 | 预处理左右最近边界及盘子前缀和,以常数时间回答区间查询。 | |
| 2602 | 使数组元素全部相等的最少操作次数 | 排序配合前缀和与分界位置,将每次绝对差总成本拆成左右两段。 |
进阶 3
| # | 题目 | 一句话思路 | 难度分 |
|---|---|---|---|
| 759 | 员工空闲时间 🔒 | 合并多员工忙碌区间,再从合并结果提取有限的公共空闲间隙。 | |
| 1851 | 包含每个查询的最小区间 | 对查询离线排序并维护候选区间长度堆,及时移除过期区间。 | |
| 1074 | 元素和为目标值的子矩阵数量 | 固定两条边压缩二维矩阵,再复用一维前缀频次统计目标和。 |
本站 OA 真题 187
- Earliest Common Meeting SlotMeta
- Sliding Window AverageMeta
- Get Minimum Round Trip CostMeta
- Combine Two Vectors of Intervals (MLE)Meta
- Cart Prefix ZeroAmazon
- Consistency ScoreAmazon
- Get Total BalancedAmazon
- Get Smaller ItemsAmazon
- Find Max GreynessAmazon
- Get Min Connection CostAmazon