差分数组(Difference Array)是一种高效处理区间更新的技巧。当需要对数组的某个区间 [l, r] 内的所有元素同时加上一个值 v 时,暴力方法需要 O(r-l+1) 时间,而差分数组只需 O(1) 时间。本文用Java实现差分数组,讲解其原理、区间更新操作与前缀和还原,并结合实际例题展示应用场景。
核心原理
差分数组 diff 与原数组 arr 的关系:
diff[0] = arr[0]
diff[i] = arr[i] - arr[i-1] (i > 0)
反过来,原数组可以通过前缀和还原:
arr[i] = diff[0] + diff[1] + ... + diff[i]
关键性质:对原数组区间 [l, r] 每个元素加 v,等价于:
– diff[l] += v
– diff[r+1] -= v(如果 r+1 在范围内)
这样单次区间更新仅需两次单点修改,复杂度为 O(1)。
为什么差分数组有效
原数组: [3, 3, 3, 3, 3, 3]
差分数组: [3, 0, 0, 0, 0, 0]
对[1,3]加2:
diff[1] += 2 → [3, 2, 0, 0, 0, 0]
diff[4] -= 2 → [3, 2, 0, 0, -2, 0]
前缀和还原: [3, 5, 5, 5, 3, 3]
↑ ↑ ↑
只有这三个位置增加了2
Java完整实现
import java.util.Arrays;
/**
* 差分数组(Difference Array)
* 高效处理区间更新,单次更新O(1),最终还原O(n)
*/
public class DifferenceArray {
private long[] diff; // 差分数组
private int n; // 数组长度
/**
* 从原数组构建差分数组
*/
public DifferenceArray(int[] arr) {
this.n = arr.length;
this.diff = new long[n];
diff[0] = arr[0];
for (int i = 1; i < n; i++) {
diff[i] = arr[i] - arr[i - 1];
}
}
/**
* 区间更新:对[l, r]区间内每个元素加上value
* 时间复杂度: O(1)
*/
public void rangeAdd(int l, int r, long value) {
if (l < 0 || r >= n || l > r) {
throw new IllegalArgumentException("无效的区间范围");
}
diff[l] += value;
if (r + 1 < n) {
diff[r + 1] -= value;
}
}
/**
* 单点更新:对位置idx加上value
* 等价于 rangeAdd(idx, idx, value)
*/
public void pointAdd(int idx, long value) {
rangeAdd(idx, idx, value);
}
/**
* 前缀和还原:从差分数组还原出最终数组
* 时间复杂度: O(n)
*/
public long[] reconstruct() {
long[] result = new long[n];
result[0] = diff[0];
for (int i = 1; i < n; i++) {
result[i] = result[i - 1] + diff[i];
}
return result;
}
/**
* 获取某个位置的当前值(不还原整个数组)
* 时间复杂度: O(n),若频繁查询建议先还原
*/
public long getValue(int idx) {
long sum = 0;
for (int i = 0; i <= idx; i++) {
sum += diff[i];
}
return sum;
}
/**
* 打印差分数组(调试用)
*/
public void printDiff() {
System.out.println("差分数组: " + Arrays.toString(diff));
}
/**
* ========== 二维差分数组 ==========
* 用于矩阵的矩形区域更新
*/
public static class Diff2D {
private long[][] diff;
private int rows, cols;
public Diff2D(int rows, int cols) {
this.rows = rows;
this.cols = cols;
this.diff = new long[rows + 2][cols + 2]; // 多开一圈避免越界判断
}
/**
* 矩形区域更新:对[x1,y1]到[x2,y2]的矩形区域每个元素加value
* 时间复杂度: O(1)
*/
public void rangeAdd(int x1, int y1, int x2, int y2, long value) {
diff[x1][y1] += value;
diff[x2 + 1][y1] -= value;
diff[x1][y2 + 1] -= value;
diff[x2 + 1][y2 + 1] += value;
}
/**
* 还原二维数组
*/
public long[][] reconstruct() {
long[][] result = new long[rows][cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
long val = diff[i][j];
if (i > 0) val += result[i - 1][j];
if (j > 0) val += result[i][j - 1];
if (i > 0 && j > 0) val -= result[i - 1][j - 1];
result[i][j] = val;
}
}
return result;
}
}
public static void main(String[] args) {
// ========== 一维差分数组演示 ==========
System.out.println("=== 一维差分数组演示 ===");
int[] arr = {0, 0, 0, 0, 0, 0, 0, 0};
DifferenceArray da = new DifferenceArray(arr);
da.printDiff();
// 执行多次区间更新
da.rangeAdd(1, 3, 5); // [0, 5, 5, 5, 0, 0, 0, 0]
da.rangeAdd(2, 5, 3); // [0, 5, 8, 8, 3, 3, 0, 0]
da.rangeAdd(0, 7, 1); // [1, 6, 9, 9, 4, 4, 1, 1]
da.rangeAdd(4, 4, 10); // [1, 6, 9, 9, 14, 4, 1, 1]
da.printDiff();
long[] result = da.reconstruct();
System.out.println("还原结果: " + Arrays.toString(result));
// 验证
System.out.println("验证:");
System.out.println(" 位置0: 0+1=1 ✓");
System.out.println(" 位置1: 0+5+1=6 ✓");
System.out.println(" 位置2: 0+5+3+1=9 ✓");
System.out.println(" 位置4: 0+3+1+10=14 ✓");
// ========== 二维差分数组演示 ==========
System.out.println("\n=== 二维差分数组演示 ===");
Diff2D diff2d = new Diff2D(5, 5);
// 在(1,1)到(3,3)区域加1
diff2d.rangeAdd(1, 1, 3, 3, 1);
// 在(2,2)到(4,4)区域加2
diff2d.rangeAdd(2, 2, 4, 4, 2);
long[][] matrix = diff2d.reconstruct();
System.out.println("二维还原结果:");
for (long[] row : matrix) {
System.out.println(Arrays.toString(row));
}
// ========== 经典例题:航班预订统计 ==========
System.out.println("\n=== 例题:航班预订统计 ===");
// bookings[i] = [first, last, seats] 表示从first到last航班预订seats个座位
int[][] bookings = {{1, 2, 10}, {2, 3, 20}, {2, 5, 25}};
int nFlights = 5;
int[] answer = corpFlightBookings(bookings, nFlights);
System.out.println("航班预订: " + Arrays.deepToString(bookings));
System.out.println("每航班座位: " + Arrays.toString(answer));
// 期望: [10, 55, 45, 25, 25]
}
/**
* 经典例题:航班预订统计(LeetCode 1109)
* 使用差分数组解决
*/
public static int[] corpFlightBookings(int[][] bookings, int n) {
long[] diff = new long[n + 2];
// 所有预订转化为差分数组操作
for (int[] booking : bookings) {
int first = booking[0] - 1; // 转为0索引
int last = booking[1] - 1;
int seats = booking[2];
diff[first] += seats;
diff[last + 1] -= seats;
}
// 前缀和还原
int[] result = new int[n];
long sum = 0;
for (int i = 0; i < n; i++) {
sum += diff[i];
result[i] = (int) sum;
}
return result;
}
/**
* 进阶例题:拼车判断(LeetCode 1094)
* 判断车辆容量是否足够承载所有乘客
*/
public static boolean carPooling(int[][] trips, int capacity) {
// 最多1000个站点
long[] diff = new long[1001];
for (int[] trip : trips) {
int num = trip[0];
int from = trip[1];
int to = trip[2];
diff[from] += num;
diff[to] -= num; // 到to站点时乘客已下车
}
long current = 0;
for (long d : diff) {
current += d;
if (current > capacity) {
return false;
}
}
return true;
}
}
进阶应用:区间最值与差分约束
区间加法与单点查询
差分数组天然支持区间加法与单点查询。如果需求是”区间加、区间求和”,可以结合树状数组或线段树实现。
差分约束系统
差分数组的思想是差分约束系统的特例。对于不等式 x[i] - x[i-1] <= c,可以用最短路算法求解。差分数组则处理等式形式的约束。
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 构建差分数组 | O(n) | O(n) |
| 区间更新 | O(1) | O(1) |
| 单点查询 | O(n)(不还原)/ O(1)(还原后) | O(n) |
| 还原整个数组 | O(n) | O(n) |
对于 m 次区间更新 + 1 次最终查询:
– 暴力方法:O(m · n)
– 差分数组:O(m + n)
当 m 很大时(如 10⁵ 次更新),差分数组的优势极其明显。
适用场景总结
差分数组最适合以下场景:
- 频繁区间更新,少量最终查询:如航班预订、教室占用统计
- 二维矩形区域更新:如图像处理中的区域加值、游戏地图的范围伤害
- 作为高级数据结构的基础:如线段树懒标记的底层原理就是差分思想
- 与前缀和配合使用:差分是前缀和的逆运算,两者常组合使用
掌握差分数组后,可以将 O(n) 的区间操作优化到 O(1),是算法竞赛和工程开发中不可或缺的技巧。