算法

前缀和 & 差分数组

O(n) 预处理换取 O(1) 区间查询,离线批量更新的最佳拍档

概览

前缀和:用 O(n) 预处理,把区间求和从 O(n) 降到 O(1)。差分数组是前缀和的逆运算:用 O(1) 完成区间加法,O(n) 重建数组。两者都是以「空间换时间」的思路,是处理区间问题的基础工具。

对偶关系

前缀和数组的「差分」就是原数组;差分数组的「前缀和」就是原数组。即 diff[i] = nums[i] − nums[i-1],prefix[i] = prefix[i-1] + nums[i-1]。理解这个互逆关系,两种结构自然融会贯通。

// Prefix sum  →  diff of prefix = original
prefix[i] = prefix[i-1] + nums[i-1]

// Difference array  →  prefix of diff = original
diff[i] = nums[i] - nums[i-1]

使用场景对比

结构适用场景复杂度
一维前缀和多次区间求和查询(只读数组)O(n) 建,O(1) 查
前缀和 + 哈希统计满足条件的子数组数量O(n) 时间空间
二维前缀和矩形区域求和查询O(mn) 建,O(1) 查
差分数组多次区间加法,最后统一查询O(n+q) 总时间

一维前缀和

预先计算累积和数组 prefix[],使得任意区间 [l, r] 的和可在 O(1) 内得出。

核心思想

定义 prefix[i] = nums[0] + nums[1] + ... + nums[i-1](1-indexed,prefix[0] = 0)。则区间 [l, r](0-indexed)的和为 prefix[r+1] - prefix[l]。用前缀相减抵消了公共前缀,无需重复相加。

步骤

  1. 建立 prefix 数组,大小为 n+1,prefix[0] = 0
  2. for i in [0, n): prefix[i+1] = prefix[i] + nums[i]
  3. 区间查询:sum(l, r) = prefix[r+1] - prefix[l](0-indexed 闭区间)
  4. 多次查询时,只需建一次 prefix,每次查询 O(1)

C++ 实现

// Build prefix sum (1-indexed)
// prefix[i] = nums[0] + nums[1] + ... + nums[i-1]
vector<long long> buildPrefix(vector<int>& nums) {
    int n = nums.size();
    vector<long long> prefix(n + 1, 0);
    for (int i = 0; i < n; i++)
        prefix[i + 1] = prefix[i] + nums[i];
    return prefix;
}

// Range sum query [l, r] inclusive (0-indexed)
long long rangeSum(vector<long long>& prefix, int l, int r) {
    return prefix[r + 1] - prefix[l];
}

常见错误

下标偏移prefix 是 1-indexed:prefix[i] 对应 nums[0..i-1] 的和,查询 [l,r] 用 prefix[r+1]-prefix[l]
数据溢出nums[i] 较大时 prefix 应用 long long,而非 int
修改后失效前缀和只适合静态数组;若 nums 频繁修改,改用树状数组(BIT)
练习:区间求和查询代码练习
测试用例(5 个)
用例 1
输入:5 1 2 3 4 5 0 2
期望:6
用例 2
输入:5 1 2 3 4 5 1 3
期望:9
用例 3
输入:5 1 2 3 4 5 0 4
期望:15
用例 4
输入:3 -1 2 3 0 2
期望:4
用例 5
输入:4 2 4 6 8 1 2
期望:10

前缀和 + 哈希表

在遍历时维护当前前缀和,并用哈希表记录每个前缀和出现的次数。可在 O(n) 内统计满足条件的子数组数目。

核心思想

子数组 [i, j] 的和 = prefixSum[j+1] - prefixSum[i]。若要找和等于 k 的子数组,等价于找满足 prefixSum[i] = prefixSum[j+1] - k 的 i。在遍历 j 时,用哈希表存所有已见的 prefixSum[i],即可 O(1) 查询。

步骤(子数组和等于 k)

  1. 初始化哈希表 cnt,令 cnt[0] = 1(空前缀和为 0,计数 1)
  2. 维护 prefixSum = 0,ans = 0
  3. 遍历每个 nums[j]:prefixSum += nums[j]
  4. ans += cnt[prefixSum - k](查找已有的配对前缀)
  5. cnt[prefixSum]++(记录当前前缀和)
  6. 返回 ans

C++ 实现

// Count subarrays with sum exactly equal to k
int subarraySum(vector<int>& nums, int k) {
    unordered_map<long long, int> cnt;
    cnt[0] = 1;           // empty prefix has sum 0
    long long prefixSum = 0;
    int ans = 0;
    for (int x : nums) {
        prefixSum += x;
        ans += cnt[prefixSum - k]; // look up existing matching prefix
        cnt[prefixSum]++;          // record current prefix (AFTER lookup!)
    }
    return ans;
}

常见变体

和能被 k 整除的子数组(LC 974)
将 prefixSum 改为 prefixSum % k,哈希表存 mod 值;负数取模需 (x%k+k)%k
和为 0 的子数组数量
k=0 的特例,cnt[0]=1 确保了单个 0 元素也被计入
和在区间 [lo, hi] 内
转换为两次前缀和查询之差(通常用离散化或归并排序)

常见错误

忘记 cnt[0]=1若缺少初始化,形如 [x] 这种从起点出发的子数组会漏计
先查后记必须先 ans+=cnt[prefixSum-k],再 cnt[prefixSum]++,否则会把自身算进去
负数取模C++ 的 % 对负数返回负值,处理「能被 k 整除」时需 (x%k+k)%k
练习:和为 k 的子数组数目代码练习
测试用例(5 个)
用例 1
输入:5 2 1 1 1 1 1
期望:4
用例 2
输入:3 3 1 2 3
期望:2
用例 3
输入:4 0 0 0 0 0
期望:10
用例 4
输入:3 5 1 2 3
期望:1
用例 5
输入:2 3 1 2
期望:1

二维前缀和

将一维前缀和推广到矩阵,用容斥原理在 O(1) 内查询任意矩形区域的元素之和。

核心思想

定义 p[i][j] = 矩形 [0,0] 到 [i-1,j-1] 的元素和(1-indexed)。建立公式:p[i][j] = grid[i-1][j-1] + p[i-1][j] + p[i][j-1] - p[i-1][j-1]。查询 [r1,c1] 到 [r2,c2](0-indexed):p[r2+1][c2+1] - p[r1][c2+1] - p[r2+1][c1] + p[r1][c1]。

// Inclusion-exclusion (2D):
//
//   p[i][j] = ████ (big rect)
//           = grid[i-1][j-1]
//           + p[i-1][j]   (rect above)
//           + p[i][j-1]   (rect to left)
//           - p[i-1][j-1] (overlap counted twice)
//
// Query [r1,c1]..[r2,c2]:
//   p[r2+1][c2+1]        (big rect)
//   - p[r1][c2+1]         (top strip)
//   - p[r2+1][c1]         (left strip)
//   + p[r1][c1]           (corner added back)

步骤

  1. 建 (m+1)×(n+1) 的 prefix 数组,所有值初始为 0
  2. 二重循环填充:p[i][j] = grid[i-1][j-1] + p[i-1][j] + p[i][j-1] - p[i-1][j-1]
  3. 查询矩形 [r1,c1,r2,c2](0-indexed):
  4. → p[r2+1][c2+1] - p[r1][c2+1] - p[r2+1][c1] + p[r1][c1]

C++ 实现

// Build 2D prefix sum (1-indexed)
// p[i][j] = sum of rectangle grid[0..i-1][0..j-1]
vector<vector<long long>> build2D(vector<vector<int>>& g) {
    int m = g.size(), n = g[0].size();
    vector<vector<long long>> p(m+1, vector<long long>(n+1, 0));
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= n; j++)
            p[i][j] = g[i-1][j-1] + p[i-1][j] + p[i][j-1] - p[i-1][j-1];
    return p;
}

// Query sum of rectangle [r1,c1]..[r2,c2] (0-indexed, inclusive)
long long query2D(vector<vector<long long>>& p, int r1, int c1, int r2, int c2) {
    return p[r2+1][c2+1] - p[r1][c2+1] - p[r2+1][c1] + p[r1][c1];
}

常见错误

下标混淆grid 是 0-indexed,p 是 1-indexed;查询时注意转换:p[r+1][c+1] 对应 grid[r][c]
容斥方向加了两次 p[i-1][j-1] 的部分要减回去,容斥公式少减或多减均出错
大矩阵溢出m×n 元素均为 10^4 级别时 p[i][j] 可能超 int,改用 long long
练习:矩形区域元素之和代码练习
测试用例(4 个)
用例 1
输入:3 3 1 2 3 4 5 6 7 8 9 0 0 1 1
期望:12
用例 2
输入:3 3 1 2 3 4 5 6 7 8 9 1 1 2 2
期望:28
用例 3
输入:2 2 1 2 3 4 0 0 1 1
期望:10
用例 4
输入:3 3 1 2 3 4 5 6 7 8 9 0 0 2 2
期望:45

差分数组

对原数组做「差分」得到 diff[],每次区间 [l, r] 加 val 只需 O(1):diff[l]+=val, diff[r+1]-=val。全部更新完毕后,对 diff 求前缀和即可重建原数组。

核心思想

diff[i] = nums[i] - nums[i-1](diff[0] = nums[0])。对 [l, r] 加 val:让 diff[l]+=val(l 处开始生效)、diff[r+1]-=val(r+1 处抵消)。最终对 diff 求一次前缀和还原出修改后的 nums。适合「离线批量区间更新,最后统一输出」的场景。

// Range add val to [l, r]:
//
//  diff: [0  0  0  0  0]
//              +val      -val
//  after add [1,3] val=2:
//  diff: [0  2  0  0 -2]
//
//  prefix sum → nums: [0  2  2  2  0]  ✓

步骤

  1. 构建 diff:diff[0]=nums[0],diff[i]=nums[i]-nums[i-1] (i>=1)
  2. 对每次区间更新 (l, r, val):diff[l]+=val;若 r+1<n 则 diff[r+1]-=val
  3. 所有更新完成后,对 diff 求前缀和重建 nums:nums[i]=nums[i-1]+diff[i]
  4. 输出最终数组

C++ 实现

// Build difference array
// diff[i] = nums[i] - nums[i-1]  (diff[0] = nums[0])
vector<int> buildDiff(vector<int>& nums) {
    int n = nums.size();
    vector<int> diff(n);
    diff[0] = nums[0];
    for (int i = 1; i < n; i++) diff[i] = nums[i] - nums[i-1];
    return diff;
}

// Range add val to [l, r] (0-indexed, inclusive) — O(1)
void rangeAdd(vector<int>& diff, int l, int r, int val) {
    diff[l] += val;
    if (r + 1 < (int)diff.size()) diff[r + 1] -= val;
}

// Rebuild original array by prefix-summing diff — O(n)
vector<int> rebuild(vector<int>& diff) {
    int n = diff.size();
    vector<int> nums(n);
    nums[0] = diff[0];
    for (int i = 1; i < n; i++) nums[i] = nums[i-1] + diff[i];
    return nums;
}

差分数组 vs 线段树

结构特点复杂度
差分数组只支持离线(批量更新后统一查询)O(n+q),实现极简
线段树支持在线(边更新边查询)O((n+q) log n),实现复杂

常见错误

r+1 越界diff[r+1]-=val 时需判断 r+1 < n,否则访问越界
忘记重建更新完 diff 后必须求一次前缀和重建 nums,不能直接读 diff
误用于查询差分数组只支持批量更新后统一查询;若需要随时查询某点,仍需求前缀和
练习:区间批量加法代码练习
测试用例(4 个)
用例 1
输入:5 2 0 0 0 0 0 1 3 2 2 4 3
期望:0 2 5 5 3
用例 2
输入:4 1 1 2 3 4 0 2 5
期望:6 7 8 4
用例 3
输入:3 3 0 0 0 0 2 1 1 2 2 0 1 3
期望:4 6 3
用例 4
输入:5 0 1 2 3 4 5
期望:1 2 3 4 5

LeetCode 题型

按技术分类的高频前缀和题目。

一维前缀和

303. 区域和检索 - 数组不可变
前缀和模板题
525. 连续数组
把 0 改为 -1,转化为前缀和=0 的子数组
1991. 找到数组的中间位置
前缀和 == 总和 - 前缀和 - nums[i]

前缀和 + 哈希表

560. 和为 K 的子数组
cnt[prefixSum-k] 计数
974. 和可被 K 整除的子数组
mod 余数做 key,注意负数取模
437. 路径总和 III
树上前缀和(DFS + 哈希表)

二维前缀和

304. 二维区域和检索 - 矩阵不可变
二维前缀和模板题
1314. 矩阵区域和
对每个位置查询周围 k 范围内的矩形和
363. 矩形区域不超过 K 的最大数值和
枚举上下边界 + 前缀和 + 二分/有序集合

差分数组

1109. 航班预订统计
区间 [first,last] 加 seats,最后还原
370. 区间加法
差分模板题(LeetCode 会员题)
1094. 拼车
差分判断任意时刻是否超载