前缀和与差分#

  • 前缀和:预处理 \(O(n)\),之后每次区间和查询 \(O(1)\),以空间换时间。

  • 差分数组:前缀和的逆运算,用于 \(O(1)\) 完成区间增量,最后前缀和还原。

  • 二者是”常用数据结构”题单的第一类,也是子数组问题的两大基础工具。

一、一维前缀和#

pre[i] = a[0] + a[1] + ... + a[i-1]pre[0] = 0,长度 n+1,避免越界判断),则区间 [l, r] 的和为:

\[ \text{sum}(l, r) = pre[r+1] - pre[l] \]
vector<int> a = {1, 2, 3, 4};
int n         = a.size();
vector<int> pre(n + 1, 0);
for (int i = 0; i < n; i++)
    pre[i + 1] = pre[i] + a[i];

// 区间 [l, r] 的和
auto rangeSum = [&](int l, int r) {
    return pre[r + 1] - pre[l];
};
int[] a = {1, 2, 3, 4};
int n = a.length;
int[] pre = new int[n + 1];
for (int i = 0; i < n; i++)
    pre[i + 1] = pre[i] + a[i];

// 区间 [l, r] 的和
int rangeSum(int l, int r) { return pre[r + 1] - pre[l]; }

二、二维前缀和#

子矩阵和用容斥(加减重叠区):

\[ S(x_1,y_1,x_2,y_2) = pre[x_2+1][y_2+1] - pre[x_1][y_2+1] - pre[x_2+1][y_1] + pre[x_1][y_1] \]
// pre[i+1][j+1] = pre[i][j+1] + pre[i+1][j] - pre[i][j] + a[i][j]
vector<vector<int>> pre(m + 1, vector<int>(n + 1, 0));
for (int i = 0; i < m; i++)
    for (int j = 0; j < n; j++)
        pre[i + 1][j + 1] = pre[i][j + 1] + pre[i + 1][j] - pre[i][j] + a[i][j];

三、差分数组#

差分数组 diffdiff[i] = a[i] - a[i-1]。对区间 [l, r] 整体加 v,只需:

\[ diff[l] += v, \quad diff[r+1] -= v \]

最后对 diff 求前缀和即还原原数组。用于多次区间增量 + 一次查询的场景(如航班预订统计 LC1109)。

// 对长度为 n 的数组做 k 次区间 [l, r] 加 v,最后输出结果数组
vector<int> diff(n + 1, 0); // 多开一位避免 r+1 越界
for (auto& op : ops) {      // ops: {l, r, v}
    int l = op[0], r = op[1], v = op[2];
    diff[l]     += v;
    diff[r + 1] -= v;
}
vector<int> ans(n, 0);
int cur = 0;
for (int i = 0; i < n; i++) {
    cur    += diff[i];
    ans[i]  = cur;
}
int[] diff = new int[n + 1];
for (int[] op : ops) {        // ops: {l, r, v}
    diff[op[0]] += op[2];
    diff[op[1] + 1] -= op[2];
}
int[] ans = new int[n];
int cur = 0;
for (int i = 0; i < n; i++) {
    cur += diff[i];
    ans[i] = cur;
}

四、前缀和 + 哈希表#

“和为 k 的子数组个数”(LC560)这类问题,前缀和配合哈希表把两数之差问题转为查找

\[ pre[r] - pre[l] = k \iff pre[l] = pre[r] - k \]

遍历时用哈希表记录每个前缀和出现的次数,累计 cnt[pre[r] - k] 即可。

int subarraySum(vector<int>& nums, int k) {
    unordered_map<int, int> cnt; // 前缀和 -> 出现次数
    cnt[0]  = 1;                 // 前缀和为 0 出现一次(空前缀)
    int sum = 0, ans = 0;
    for (int x : nums) {
        sum += x;
        ans += cnt[sum - k]; // 之前有多少个前缀和 = sum - k
        cnt[sum]++;
    }
    return ans;
}
int subarraySum(int[] nums, int k) {
    Map<Integer, Integer> cnt = new HashMap<>();
    cnt.put(0, 1);
    int sum = 0, ans = 0;
    for (int x : nums) {
        sum += x;
        ans += cnt.getOrDefault(sum - k, 0);
        cnt.merge(sum, 1, Integer::sum);
    }
    return ans;
}

Note

这类技巧被称为”枚举右,维护左”:对双变量问题,枚举右边的元素,把左边需要的信息用哈希表(或有序集合)维护, 将双变量问题降为单变量问题。两数之和、和可被 k 整除的子数组等均可套用。