mobile wallpaper 1
mobile wallpaper 2
mobile wallpaper 3
mobile wallpaper 4
1570 字
4 分钟
前缀和与差分
2026-03-12

前缀和与差分#

前缀和与差分常用于处理区间计算和区间修改问题,其核心都是将区间计算转变为单点计算。二者互为逆运算:前缀和负责”快速回答区间查询”——一次区间求和 O(1);差分负责”快速完成区间修改”——一次区间加减 O(1),全部修改结束后再花 O(n) 做一次前缀和把原数组整体还原出来。

两个技巧都遵循同一个套路:先花 O(n) 把原数组预处理成一个辅助数组,然后让每一次区间操作都变成只改动辅助数组上的常数个位置。当一道题需要对同一个数组反复进行大量区间查询或区间修改(比如 q 次操作)时,朴素的逐元素遍历总代价是 O(n·q),而前缀和/差分能把代价降到 O(n + q),这是最经典、最常用的”空间换时间”优化手段。

一维前缀和#

给定数组 a[1..n],定义前缀和数组 S[i] = a[1] + a[2] + … + a[i],即”从第 1 个元素一直累加到第 i 个元素”的和。

预处理(O(n)): 利用递推式 S[i] = S[i-1] + a[i] 从前往后扫一遍即可求出整个 S 数组。同时人为规定 S[0] = 0——下标 0 处补一个 0 是重要的边界约定:它让 i = 1 时递推式依然成立,也让我们查询”从头开始的区间”时无需任何特判。

查询(O(1)): 性质:a[l..r] 的和 = S[r] - S[l-1],一次查询 O(1)

为什么这个公式成立?把两个前缀和展开:

S[r] = a[1] + a[2] + … + a[l-1] + a[l] + a[l+1] + … + a[r]
S[l-1] = a[1] + a[2] + … + a[l-1]

两式相减,公共部分 a[1..l-1] 恰好被抵消,剩下的正好是 a[l] + a[l+1] + … + a[r]。也就是说,计算 [a,b] 的区间和公式为:

a[a] + a[a+1] + a[a+2] + ... + a[b] = S[b] - S[a-1]

一次区间求和由 O(区间长度) 降为 O(1),代价仅仅是在读入数组时顺手把前缀和数组算出来。

注意: 前缀和的前提是原数组在查询期间不再变化。如果一边查询一边修改原数组,前缀和会全部失效,此时要改用下面的差分(只修改、最后查询),或者进一步学习树状数组/线段树等支持动态修改的数据结构。

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
//一维前缀和
Scanner sc = new Scanner(System.in);
int n= 10;
int[] arr = new int[n];
int[] s = new int[n];
//前缀和从一开始,下标为0的值为0
s[0] = 0;
for (int i = 1; i < n; i++) {
arr[i] = sc.nextInt();
}
for (int i = 1; i < n; i++) {
s[i] = s[i-1] + arr[i];
}
for (int i = 0; i < n; i++) {
System.out.println(s[i]);
}
int l = 3;
int r = 8;
//计算数组第三位到第八位的和,时间复杂度为o(1),前缀和数组初始化时间为o(n)
System.out.println(s[r] - s[l-1]);
}
}

一维差分#

想在原数组 a[l..r]同时加/减一个值 c,正常做法要逐个修改 r-l+1 个元素,即 O(r-l+1)。

引入差分数组 D[i] = a[i] - a[i-1](规定 a[0] = 0)。差分与前缀和互为逆运算:对 D 做一次前缀和,就能把 a 原样还原出来,因为

D[1] + D[2] + … + D[i] = (a[1]-a[0]) + (a[2]-a[1]) + … + (a[i]-a[i-1]) = a[i]

中间项两两抵消,最后只剩 a[i]

区间加 c 只需改两个端点:

D[l] += c;
D[r+1] -= c;

为什么只改两个位置就够了?模拟一下”对 D 求前缀和还原 a”的过程:

  • 当还原到下标 i < l 时,前缀和中还没有累加进 D[l] 处的 c,这些位置保持不变;
  • l ≤ i ≤ r 时,前缀和中额外多了一个来自 D[l] 的 c,所以这段区间内的每个元素都被 +c,这正是我们想要修改的区间;
  • i > r 时,D[r+1] 处的 -c 开始被累加进来,把之前的 +c 抵消掉,后面的元素恢复原值。

举个具体例子:a = [1,2,3,4,5](下标 15),差分数组 D = [1,1,1,1,1]。对区间 [2,4] 加 10,只需 D[2] += 10D[5] -= 10,D 变为 [1,11,1,1,-9];再求前缀和还原得 [1,12,13,14,5],恰好只有第 24 位加了 10。

适用场景: 对同一个数组做多次区间修改时,每次修改都只是 O(1) 的端点操作,全部修改完成后再做一次 O(n) 的前缀和还原。m 次修改的总代价是 O(n + m),远优于朴素的 O(n·m)。

最后做一次前缀和即可还原原数组。

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
//一维差分
Scanner sc = new Scanner(System.in);
int[] D = new int[100];
int[] a = new int[100];
for (int i = 0; i < 10; i++) {
a[i] = sc.nextInt();
}
// 初始化差分数组
for (int i = 1; i <= 10; ++i) D[i] = a[i] - a[i-1];
//将数组第一个到第六个数都加上3
D[1] += 3;
D[7] -= 3;
// 还原数组
for (int i = 1; i <= 10; ++i) a[i] = a[i-1] + D[i];
for (int i = 0; i < 10; i++) {
System.out.println(a[i]);
}
}
}

二维差分#

二维前缀和: S[i][j] 表示从左上角 (1,1)(i,j) 的矩形内所有元素之和。

它同样可以用 O(n·m) 递推求出:S[i][j] = S[i-1][j] + S[i][j-1] - S[i-1][j-1] + a[i][j],即”上方矩形 + 左方矩形 − 两者重叠的左上矩形 + 自身”。这里减掉一次重叠部分,就是典型的容斥原理:重叠区域被加了两次,所以要减回去一次。

查询子矩阵 (x1,y1)(x2,y2) 的和(x1 ≤ x2,y1 ≤ y2):

sum = S[x2][y2] - S[x1-1][y2] - S[x2][y1-1] + S[x1-1][y1-1]

仍然用容斥理解:先取整个大矩形 (1,1)~(x2,y2),减去上方多余部分 (1,1)~(x1-1,y2) 和左方多余部分 (1,1)~(x2,y1-1);此时左上角小矩形 (1,1)~(x1-1,y1-1) 被减了两次,所以再加回来一次。一次二维区间查询同样是 O(1)。

二维差分: 想把子矩阵 (x1,y1)~(x2,y2) 内的所有元素同时加 c,只需修改四个角:

D[x1][y1] += c;
D[x2+1][y1] -= c;
D[x1][y2+1] -= c;
D[x2+1][y2+1] +=c

可以把它看成”一维差分在二维上的推广”:D[x1][y1] += c 使得求二维前缀和时,目标矩形右下方整个区域都会被加上 c;(x2+1,y1)(x1,y2+1) 处的 -c 分别把目标区域”右方”和”下方”的多余部分截断;而最右下角的 (x2+1,y2+1) 区域同时被前两个 -c 各减了一次,共减了 2c,所以要再 +c 补回来,使它恢复原值。四个角配合,最终恰好只有目标矩形内 +c。

差分数组无需单独构造: 差分有一个很省事的构造技巧——直接用”插入”操作完成初始化。

先假设原数组 a 中所有数全为 0,那么这个数组的差分数组也全为 0;

然后对 a 数组的每个元素做插入操作(把 a[i][j] 看成”对单点区间 (i,j,i,j) 加 a[i][j]”),最后还原数组即可得到更新后的数组。这样就不需要单独推导差分数组的构造公式,所有操作统一成”插入”这一种。

一维数组插入

D[l] += c;
D[r+1] -=c;

二维数组插入

D[x1][y1] += c;
D[x2+1][y1] -= c;
D[x1][y2+1] -= c;
D[x2+1][y2+1] +=c

示例代码

1.实现一维差分对数组实现片段插入

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc= new Scanner(System.in);
int n = sc.nextInt();
int[] a = new int[1000];
int[] b= new int[1000];
for (int i = 1; i <= n; i++) {
a[i] = sc.nextInt();
}
for (int i = 1; i <= n; i++) {
insert(i,i,a[i],b);
}
insert(1,4,1,b);
//求原来数组
for (int i = 1; i <= n; i++) {
b[i] += b[i-1];
}
for (int i =1; i <= n; i++) {
System.out.println(b[i]);
}
}
public static void insert(int i,int j ,int c,int[] a){
a[i] += c;
a[j+1] -= c;
}
}

2.实现二维数组差分对数组实现范围插入

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc= new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
int[][] a = new int[100][100];
int[][] b= new int[100][100];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m ; j++) {
a[i][j] = sc.nextInt();
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <=m ; j++) {
insert(i,j,i,j,a[i][j],b);
}
}
insert(1,1,4,4,1,b);
//求原来数组
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m ; j++) {
b[i][j] = b[i-1][j] + b[i][j-1] -b[i-1][j-1] + b[i][j];
}
}
for (int i =1; i <= n; i++) {
System.out.println();
for (int j = 1; j <= m ; j++) {
System.out.print(b[i][j] + " ");
}
}
}
public static void insert(int x1,int y1,int x2,int y2 ,int c,int[][] b){
b[x1][y1] += c;
b[x2+1][y1] -=c;
b[x1][y2+1] -=c;
b[x2+1][y2+1] += c;
}
}
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

前缀和与差分
https://blogstella.xyz/posts/算法入门之前缀和与差分/
作者
Stella
发布于
2026-03-12
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录