前缀和与差分

一句话说明

前缀和把多次区间查询变成两次减法;差分把多次区间修改变成两个端点标记。

前缀和

定义 prefix[i] 为前 i 个元素之和,特意让 prefix[0] = 0:

nums:      3   1   4   2
index:     0   1   2   3
prefix: 0  3   4   8  10

区间 [left, right] 的和为:

prefix[right + 1] - prefix[left]
flowchart LR
    A[0 到 right 的总和] --> C["减去 0 到 left-1 的总和"]
    C --> D[left 到 right 的区间和]
func buildPrefix(nums []int) []int {
    prefix := make([]int, len(nums)+1)
    for i, value := range nums {
        prefix[i+1] = prefix[i] + value
    }
    return prefix
}
 
func rangeSum(prefix []int, left, right int) int {
    return prefix[right+1] - prefix[left]
}

预处理 O(n),每次查询 O(1),额外空间 O(n)。

二维前缀和

prefix[r][c] 表示左上角到 (r-1, c-1) 的矩形和。查询矩形时使用容斥:

目标矩形 = 大矩形 - 上方 - 左方 + 被重复减去的左上角
func rectangleSum(prefix [][]int, r1, c1, r2, c2 int) int {
    return prefix[r2+1][c2+1] -
        prefix[r1][c2+1] -
        prefix[r2+1][c1] +
        prefix[r1][c1]
}

差分数组

差分数组记录相邻元素的变化量:

nums:  2   2   5   5
diff:  2   0   3   0

给闭区间 [left, right] 全部增加 value,只需要:

diff[left] += value
diff[right + 1] -= value    # right + 1 存在时

最后对 diff 求一次前缀和即可还原修改后的数组。

type Update struct {
    Left  int
    Right int
    Delta int
}
 
func applyUpdates(nums []int, updates []Update) []int {
    if len(nums) == 0 {
        return nil
    }
 
    diff := make([]int, len(nums)+1)
    diff[0] = nums[0]
    for i := 1; i < len(nums); i++ {
        diff[i] = nums[i] - nums[i-1]
    }
 
    for _, update := range updates {
        diff[update.Left] += update.Delta
        diff[update.Right+1] -= update.Delta
    }
 
    result := make([]int, len(nums))
    running := 0
    for i := 0; i < len(nums); i++ {
        running += diff[i]
        result[i] = running
    }
    return result
}

使用长度 n + 1 的差分数组后,right + 1 不需要额外判断。

选择指南

问题结构单次操作
静态数组,多次区间求和前缀和查询 O(1)
多次区间加值,最后统一输出差分修改 O(1)
动态单点修改 + 区间查询[[../数据结构/树状数组树状数组]]
动态区间修改 + 区间查询[[../数据结构/线段树线段树]]

易错点

  • 统一闭区间或半开区间,不要混用。
  • 前缀数组多出的首位 0 能显著减少边界分支。
  • 差分适合离线批量修改;如果每次修改后立刻查询,通常要换数据结构。

相关主题


返回:算法基础 | 算法学习导航