前缀和数组与差分数组详解
前缀和数组(Prefix Sum Array)和差分数组(Difference Array)是一对互补的数据结构,分别用于快速区间查询和快速区间更新。
目录
前缀和数组
前缀和数组(Prefix Sum Array)是一种预处理技术,通过预处理原数组,可以在 O(1) 时间内查询任意区间的和。
概念
前缀和数组是一种预处理技术,通过预处理原数组,可以在 O(1) 时间内查询任意区间的和。
工作原理
graph LR
A["原数组
arr = [1, 2, 3, 4, 5]"] --> B["前缀和数组
pre = [0, 1, 3, 6, 10, 15]"]
B --> C["区间 [i, j] 的和
= pre[j+1] - pre[i]"]
style A fill:#e1f5ff
style B fill:#e8f5e9
style C fill:#fff3e0
核心思想
- 预处理: 构建前缀和数组
pre[i] = arr[0] + arr[1] + ... + arr[i-1] - 查询: 区间
[i, j]的和 =pre[j+1] - pre[i] - 时间复杂度: 预处理 O(n),查询 O(1)
构建过程
flowchart TD
A["原数组 arr"] --> B["初始化 pre[0] = 0"]
B --> C["遍历 i from 0 to n-1"]
C --> D["pre[i+1] = pre[i] + arr[i]"]
D --> E{是否遍历完?}
E -->|否| C
E -->|是| F["前缀和数组构建完成"]
style A fill:#ffcccc
style F fill:#ccffcc
实现示例
Go 语言实现
1 | package main |
执行流程示例
sequenceDiagram
participant U as 用户
participant PS as PrefixSum
participant A as 原数组
U->>PS: 创建前缀和数组
PS->>A: 读取原数组 [1,2,3,4,5]
A-->>PS: 返回数组
PS->>PS: 构建 pre = [0,1,3,6,10,15]
U->>PS: Query(1, 3)
PS->>PS: pre[4] - pre[1]
PS->>PS: 10 - 1 = 9
PS-->>U: 返回 9
应用场景
1. 区间求和问题
多次查询数组的区间和,使用前缀和可以将每次查询的时间复杂度从 O(n) 降低到 O(1)。
问题示例:给定一个数组,多次查询区间 [l, r] 的和。
1 | // 不使用前缀和:每次查询 O(n) |
2. 子数组问题
查找满足条件的子数组,如和等于目标值的子数组、最大子数组和等。
示例:找到所有和等于目标值的子数组
1 | func findSubarraysWithSum(arr []int, target int) [][]int { |
3. 二维前缀和
扩展到二维矩阵的区间求和。
4. 其他应用
- 统计问题:统计满足条件的区间数量
- 滑动窗口:结合滑动窗口解决相关问题
- 动态规划:作为动态规划的优化技巧
复杂度分析
- 空间复杂度: O(n)
- 预处理时间复杂度: O(n)
- 查询时间复杂度: O(1)
前缀和 vs 暴力查询
graph TB
A["查询区间和"] --> B["暴力方法"]
A --> C["前缀和方法"]
B --> B1["每次查询 O(n)"]
B --> B2["m次查询 O(mn)"]
C --> C1["预处理 O(n)"]
C --> C2["每次查询 O(1)"]
C --> C3["m次查询 O(n + m)"]
style B fill:#ffcccc
style C fill:#ccffcc
| 方法 | 预处理 | 单次查询 | m次查询 |
|---|---|---|---|
| 暴力方法 | O(1) | O(n) | O(mn) |
| 前缀和方法 | O(n) | O(1) | O(n + m) |
当查询次数 m 较大时,前缀和方法的优势明显。
前缀和变种
数组左半部和右半部的差
问题描述
给定一个数组,对于每个位置 i,计算左半部(arr[0…i-1])和右半部(arr[i+1…n-1])的差。
解决方案
使用前缀和和后缀和(或两次前缀和)来解决。
1 | // 计算每个位置左右两部分的差 |
可视化
graph TB
A["原数组: [10, 4, 8, 3]"] --> B["位置 0"]
A --> C["位置 1"]
A --> D["位置 2"]
A --> E["位置 3"]
B --> B1["左: [] = 0
右: [4,8,3] = 15
差: |0-15| = 15"]
C --> C1["左: [10] = 10
右: [8,3] = 11
差: |10-11| = 1"]
D --> D1["左: [10,4] = 14
右: [3] = 3
差: |14-3| = 11"]
E --> E1["左: [10,4,8] = 22
右: [] = 0
差: |22-0| = 22"]
style A fill:#ffcccc
style B1 fill:#ccffcc
style C1 fill:#ccffcc
style D1 fill:#ccffcc
style E1 fill:#ccffcc
前缀积
类似前缀和,但计算的是乘积。
1 | // PrefixProduct 前缀积数组 |
前缀异或
计算前缀异或和,用于解决异或相关问题。
1 | // PrefixXOR 前缀异或数组 |
前缀最大值/最小值
记录前缀的最大值或最小值。
1 | package main |
二维前缀和
二维前缀和用于快速计算矩阵中任意矩形区域的和。
概念
graph TB
A["二维矩阵
matrix[m][n]"] --> B["二维前缀和
pre[m+1][n+1]"]
B --> C["矩形 [x1,y1] 到 [x2,y2] 的和
= pre[x2+1][y2+1] - pre[x1][y2+1]
- pre[x2+1][y1] + pre[x1][y1]"]
style A fill:#e1f5ff
style B fill:#e8f5e9
style C fill:#fff3e0
构建过程
flowchart TD
A["二维矩阵"] --> B["初始化 pre[0][j] = 0, pre[i][0] = 0"]
B --> C["遍历 i from 1 to m"]
C --> D["遍历 j from 1 to n"]
D --> E["pre[i][j] = pre[i-1][j] + pre[i][j-1]
- pre[i-1][j-1] + matrix[i-1][j-1]"]
E --> F{是否遍历完?}
F -->|否| D
F -->|是| G["二维前缀和构建完成"]
style A fill:#ffcccc
style G fill:#ccffcc
实现
1 | // PrefixSum2D 二维前缀和 |
可视化
graph TB
A["矩阵
1 2 3
4 5 6
7 8 9"] --> B["二维前缀和
0 0 0 0
0 1 3 6
0 5 12 21
0 12 27 45"]
B --> C["查询 [0,0] 到 [1,1]"]
C --> D["pre[2][2] - pre[0][2]
- pre[2][0] + pre[0][0]"]
D --> E["12 - 6 - 5 + 0 = 1
实际: 1+2+4+5 = 12"]
style A fill:#ffcccc
style E fill:#ccffcc
差分数组
差分数组是前缀和的逆操作,用于快速进行区间更新操作。
概念
graph LR
A["原数组
arr = [1, 2, 3, 4, 5]"] --> B["差分数组
diff = [1, 1, 1, 1, 1]"]
B --> C["前缀和恢复
arr = prefixSum(diff)"]
style A fill:#e1f5ff
style B fill:#e8f5e9
style C fill:#fff3e0
核心思想
- 差分数组:
diff[i] = arr[i] - arr[i-1](i > 0),diff[0] = arr[0] - 前缀和恢复:对差分数组求前缀和可以得到原数组
- 区间更新:对区间 [l, r] 加 val,只需更新
diff[l] += val和diff[r+1] -= val
工作原理
graph TB
A["原数组
arr = [1, 2, 3, 4, 5]"] --> B["差分数组
diff = [1, 1, 1, 1, 1]"]
B --> C["区间 [i, j] 加 val
diff[i] += val
diff[j+1] -= val"]
C --> D["前缀和还原
arr = prefixSum(diff)"]
style A fill:#e1f5ff
style B fill:#e8f5e9
style C fill:#fff3e0
style D fill:#f3e5f5
实现示例
Go 语言实现
1 | package main |
更新流程示例
sequenceDiagram
participant U as 用户
participant DA as DifferenceArray
participant A as 原数组
U->>DA: 创建差分数组
DA->>A: 读取原数组 [1,2,3,4,5]
A-->>DA: 返回数组
DA->>DA: 构建 diff = [1,1,1,1,1]
U->>DA: Update(1, 3, 2)
DA->>DA: diff[1] += 2 → diff[1] = 3
DA->>DA: diff[4] -= 2 → diff[4] = -1
U->>DA: GetArray()
DA->>DA: 前缀和还原
DA-->>U: 返回 [1,4,5,6,5]
差分数组的优势
graph TB
A["区间更新操作"] --> B["暴力方法"]
A --> C["差分数组"]
B --> B1["每次更新 O(n)"]
B --> B2["k次更新 O(kn)"]
C --> C1["每次更新 O(1)"]
C --> C2["k次更新 O(k)"]
C --> C3["最后恢复 O(n)"]
C --> C4["总复杂度 O(k + n)"]
style B fill:#ffcccc
style C fill:#ccffcc
应用场景
1. 区间加法
对数组的多个区间进行加法操作,最后查询结果。
问题示例:给定一个数组,进行 k 次操作,每次对区间 [l, r] 的所有元素加 val,最后输出数组。
1 | func rangeAddition(arr []int, operations [][]int) []int { |
2. 航班预订统计
记录每个航班的预订数量变化。
问题描述:有 n 个航班,编号从 1 到 n。给定一个航班预订表 bookings,其中 bookings[i] = [first, last, seats] 表示在航班 first 到 last(包含 first 和 last)的每个航班上预订了 seats 个座位。返回一个长度为 n 的数组 answer,其中 answer[i] 是航班 i+1 上预订的座位总数。
1 | func corpFlightBookings(bookings [][]int, n int) []int { |
3. 会议室安排
统计每个时间段的会议数量。
问题描述:给定一系列会议的开始和结束时间,统计每个时间段的会议数量。
1 | func meetingCount(meetings [][]int, timeSlots int) []int { |
复杂度分析
- 空间复杂度: O(n)
- 构建时间复杂度: O(n)
- 更新时间复杂度: O(1)
- 查询时间复杂度: O(n)(需要前缀和还原)
二维差分
扩展到二维矩阵的区间更新。
1 | // DifferenceArray2D 二维差分数组 |
实际应用问题
问题1:子数组和等于K
问题描述:给定一个整数数组和一个整数 k,找到所有和等于 k 的连续子数组的个数。
解决方案:使用前缀和 + 哈希表
1 | func subarraySum(nums []int, k int) int { |
问题2:最大子数组和
问题描述:找到一个数组中和最大的连续子数组。
解决方案:使用前缀和 + 维护最小前缀和
1 | func maxSubArray(nums []int) int { |
问题3:区间更新问题
问题描述:给定一个数组,进行多次区间更新操作,每次对区间 [l, r] 的所有元素加 val,最后输出数组。
解决方案:使用差分数组
1 | func rangeUpdate(arr []int, updates [][]int) []int { |
问题4:二维区间求和
问题描述:给定一个二维矩阵,多次查询矩形区域的和。
解决方案:使用二维前缀和
1 | func matrixRangeSum(matrix [][]int, queries [][]int) []int { |
前缀和与其他数据结构
前缀和 vs 线段树
graph TB
A["区间查询问题"] --> B["前缀和"]
A --> C["线段树"]
B --> B1["只支持区间和查询"]
B --> B2["不支持修改"]
B --> B3["实现简单"]
B --> B4["查询 O(1)"]
C --> C1["支持多种区间操作"]
C --> C2["支持单点/区间修改"]
C --> C3["实现复杂"]
C --> C4["查询 O(log n)"]
style B fill:#ccffcc
style C fill:#ccccff
| 特性 | 前缀和 | 线段树 |
|---|---|---|
| 查询类型 | 区间和 | 区间和、最大值、最小值等 |
| 修改操作 | 不支持 | 支持单点/区间修改 |
| 查询复杂度 | O(1) | O(log n) |
| 空间复杂度 | O(n) | O(n) |
| 实现难度 | 简单 | 复杂 |
前缀和 vs 树状数组
| 特性 | 前缀和 | 树状数组 |
|---|---|---|
| 查询类型 | 区间和 | 区间和 |
| 修改操作 | 不支持 | 支持单点修改 |
| 查询复杂度 | O(1) | O(log n) |
| 空间复杂度 | O(n) | O(n) |
| 实现难度 | 简单 | 中等 |
前缀和 vs 差分数组
| 特性 | 前缀和 | 差分数组 |
|---|---|---|
| 主要用途 | 区间查询 | 区间更新 |
| 操作类型 | 查询区间和 | 更新区间值 |
| 查询复杂度 | O(1) | O(n)(需要还原) |
| 更新复杂度 | 不支持 | O(1) |
| 关系 | 差分数组的前缀和 = 原数组 | 原数组的差分 = 差分数组 |
最佳实践
1. 边界处理
1 | // 好的做法:在 Query 方法中检查边界 |
2. 索引理解
graph TB
A["原数组索引"] --> B["前缀和数组索引"]
A --> A1["arr[0], arr[1], ..., arr[n-1]"]
B --> B1["pre[0] = 0
pre[1] = arr[0]
pre[2] = arr[0]+arr[1]
...
pre[n] = arr[0]+...+arr[n-1]"]
C["区间查询"] --> D["arr[l...r] 的和"]
D --> E["pre[r+1] - pre[l]"]
style A fill:#ffcccc
style B fill:#ccffcc
style E fill:#ccffcc
关键点:
pre[i]表示arr[0...i-1]的和- 区间
[l, r]的和 =pre[r+1] - pre[l] - 从 0 到 i 的和 =
pre[i+1]
3. 负数处理
如果数组中有负数,前缀和可能为负,需要注意:
1 | // 处理负数的情况 |
4. 溢出处理
对于大数,注意整数溢出:
1 | // 使用 int64 避免溢出 |
5. 差分数组的边界处理
1 | // 差分数组更新时注意边界 |
总结
前缀和数组和差分数组是一对互补的强大数据结构:
核心优势
前缀和数组
- 快速查询:O(1) 时间查询区间和
- 简单实现:代码简洁,易于理解
- 广泛应用:适用于多种区间问题
差分数组
- 快速更新:O(1) 时间进行区间更新
- 批量操作:可以累积多次更新后统一还原
- 高效处理:适合区间更新频繁的场景
关键要点
前缀和数组
- 预处理:O(n) 时间构建前缀和数组
- 查询公式:区间 [l, r] 的和 =
pre[r+1] - pre[l] - 索引关系:
pre[i]表示arr[0...i-1]的和
差分数组
- 构建:O(n) 时间构建差分数组
- 更新公式:区间 [l, r] 加 val:
diff[l] += val,diff[r+1] -= val - 还原:对差分数组求前缀和得到原数组
适用场景
前缀和数组
- 多次区间和查询
- 子数组问题
- 二维矩阵区间求和
- 结合其他技巧(哈希表、滑动窗口等)
差分数组
- 多次区间更新操作
- 航班预订统计
- 会议室安排
- 区间加法问题
扩展方向
- 二维前缀和/差分:扩展到矩阵
- 前缀积/异或:其他运算的前缀形式
- 动态前缀和:结合树状数组或线段树
- 多维前缀和:扩展到更高维度
互补关系
前缀和数组和差分数组是互逆操作:
- 对原数组求差分得到差分数组
- 对差分数组求前缀和得到原数组
- 前缀和用于快速查询,差分数组用于快速更新
理解前缀和数组和差分数组有助于:
- 优化区间查询和更新问题
- 解决子数组相关问题
- 掌握预处理技巧
- 提升算法解题能力
参考文献
- 《算法导论》
- LeetCode 前缀和与差分数组相关题目
- 《编程珠玑》