cswork / 模板打开 500 路题单

第 05 站 · 线性结构

前缀和与区间

前缀和 + 哈希计数 时间 O(n) · 空间 O(n)

周赛难度分 难度分 1375–难度分 2286· 中位 难度分 1624(15/25 道题有 zerotrac 周赛分)

打开 500 路题单 · 第 05 站 · 25 题
先学
学完去
没有后续站依赖它。
面试频次
14 家公司考过 · 面经出现 26 次Meta 7、TikTok / ByteDance 5、Amazon 2、OpenAI 2、Adobe 1

先做这道

LC 724寻找数组的中心下标简单
卡住再看

用总和与左侧累计和计算右侧和,避免重复区间求和。

要点

  1. 区间和等于两个前缀和之差:sum(l..r) = pre[r+1] - pre[l];前缀数组多开一位,pre[0] = 0。
  2. 统计「和为 k 的子数组」时,哈希表先放 {0: 1},代表从下标 0 开始的子数组。
  3. 多次区间加值用差分数组: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 的连续子数组?

换个输入
10
21
12
-13
24
15
·pre[1]
·pre[2]
·pre[3]
·pre[4]
·pre[5]
·pre[6]
当前处理已发现 / 已计算答案
哈希表(前缀和×次数)
0×1
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 ans
C++
// 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 res
C++
// 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 res
C++
// 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
}

为什么对

  1. sum(l..r) = pre[r+1] − pre[l],所以以 r 结尾、和为 k 的子数组个数,等于之前出现过多少个等于 pre[r+1] − k 的前缀和。哈希表按前缀和计数,一遍扫描把所有子数组按右端点分组数完,每个恰好算一次。
  2. {0: 1} 代表空前缀 pre[0],没有它就会漏掉从下标 0 开始的子数组。
  3. 差分数组是前缀和的逆运算:diff[l] += v、diff[r+1] −= v 之后求一次前缀和,只有 [l, r] 内的位置加上了 v。

30 秒口述稿

面试官让你讲思路时,按做法、为什么对、复杂度的顺序说。中英文各一版,可以对着读几遍。

中文
子数组和可以写成两个前缀和的差,pre[r+1] − pre[l]。所以我从左往右扫,维护当前前缀和,同时用哈希表记录每个前缀和出现过几次;以当前位置结尾、和为 k 的子数组数量,就是之前前缀和等于当前值减 k 的次数。哈希表要先放入 {0: 1} 表示空前缀。这样每个子数组按右端点恰好统计一次,时间 O(n),而且对负数也成立。
English
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 天的间隔回来,到期的题在「我的进度 · 到期复习」里。 只存在这台设备上;登录后会合并到你的账号。

对照要点(0/3)
这道题你会了吗?

小测

5 道概念题和复杂度题。答过的题会和 OA 选择题练习一起按间隔回来复习。 去 OA 选择题练习

5 道题
  1. 前缀和数组 P 定义为 P[0] = 0,P[i] = a[0] + … + a[i-1]。闭区间 a[l..r] 的和等于?
  2. 合并一组可能重叠的区间,标准做法和时间复杂度是?
  3. 对长度为 n 的数组做 q 次「给 a[l..r] 全部加 v」的操作,最后只读一次整个数组。用差分数组,总时间是?
  4. a 长度为 n,queries 有 q 个 (l, r) 区间。时间复杂度是?
    def range_sums(a, queries):
        p = [0]
        for x in a:
            p.append(p[-1] + x)
        return [p[r + 1] - p[l] for l, r in queries]
  5. a 长度为 n,queries 有 q 个 (l, r) 区间。最坏情况下的时间复杂度是?
    def range_sums(a, queries):
        return [sum(a[l:r + 1]) for l, r in queries]

作答记录只保存在这台设备的浏览器里;登录后打开OA 选择题练习页,会存进账号。

模板练习

用上面的模板和变体出题:把打乱的行排回原序,把挖掉的边界条件填回去,或者在空白编辑器里整段默写,跑本站的测试并逐行对比。每次「换一题」都会重新出题。

下面几行被打乱了。点一行选中,再点另一行和它交换,排回模板原来的顺序;缩进是提示。

分档练习

本站在 500 路里的 25 道题,按基础、核心、进阶分三档。每题附一句话思路,先自己想,想不出再看。

基础 2

#题目一句话思路难度分
724寻找数组的中心下标用总和与左侧累计和计算右侧和,避免重复区间求和。
370区间加法 🔒把多次闭区间加法写成差分数组的两个端点更新。

核心 20

#题目一句话思路难度分
560和为 K 的子数组用前缀和频次查找差为k的历史状态,支持负数与重复前缀。
525连续数组把零替换为负一,再用最早相同前缀位置求最长平衡区间。
523连续的子数组和记录余数首次位置,通过重复余数与距离判断长度至少二的区间。
974和可被 K 整除的子数组统计相同前缀余数的组合,并统一负数取模的表示。难度分 1676
930和相同的二元子数组用前缀频次统计二进制数组目标和,避免连续零带来的漏计。难度分 1592
1248统计「优美子数组」把奇偶性映射为零一值,再统计恰好k个奇数的区间。难度分 1624
1423可获得的最大点数把两端选取转化为删除中间固定长度窗口,求总和减最小窗口和。难度分 1574
1109航班预订统计用差分记录批量航班区间增量,通过一次前缀恢复最终值。难度分 1570
1094拼车按站点累计上下车事件,准确处理同站先下车再上车的容量边界。难度分 1441
2381字母移位 II用带符号差分批量移动字符,并正确规范化负向循环位移。难度分 1793
56合并区间按起点排序合并相交区间,维护当前区间的最远终点。
57插入区间在线性扫描中区分新插入区间之前、相交及之后三段。
986区间列表的交集双指针求两个有序不交区间列表的交集,每次推进终点较早者。难度分 1542
435无重叠区间按结束时间进行区间选择,把最少删除转成最多保留。
452用最少数量的箭引爆气球把射箭位置选在最早结束边界,正确处理闭区间端点重合。
253会议室 II 🔒用结束时间堆或扫描线计算同时占用的最少会议室数。
1288删除被覆盖区间起点升序、同起点终点降序后扫描,准确判断区间覆盖。难度分 1375
1314矩阵区域和构建二维前缀和,用四项容斥回答截断边界的矩形和。难度分 1484
2055蜡烛之间的盘子预处理左右最近边界及盘子前缀和,以常数时间回答区间查询。难度分 1819
2602使数组元素全部相等的最少操作次数排序配合前缀和与分界位置,将每次绝对差总成本拆成左右两段。难度分 1903

进阶 3

#题目一句话思路难度分
759员工空闲时间 🔒合并多员工忙碌区间,再从合并结果提取有限的公共空闲间隙。难度分 1710
1851包含每个查询的最小区间对查询离线排序并维护候选区间长度堆,及时移除过期区间。难度分 2286
1074元素和为目标值的子矩阵数量固定两条边压缩二维矩阵,再复用一维前缀频次统计目标和。难度分 2189

Core Skills

不调库,亲手实现这一站用到的数据结构或算法,四种语言都能判题。