差分数组的思想和前缀和数组类似,主要适用场景是频繁对原始数组的某个区间的元素进行增减。举个例子就是:给一个数组nums,要求对nums一些区间进行若干次区间内元素的全部加或者减,比nums=[3,2,4,5,8,2,4,6,8,7],要求对下标1-5区间内元素全部加4,再对下标3-9区间内元素全部+3,……,执行若干次这样的操作后,问最终的nums数组元素是什么样的?
很容易想到对每一次操作都执行一次for循环给元素赋值,但是如果这样的操作多了起来之后,每次时间复杂度都是O(n),效率低。这时候就可以构造一个差分数组优化时间了。
可以构造一个差分数组diff,diff[i] 就是 nums[i] 和 nums[i-1] 之差:
int[] diff = new int[nums.length];
// 构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
nums | 4 | 1 | 9 | 7 | 2 |
---|---|---|---|---|---|
diff | 4 | -3 | 8 | -2 | -5 |
通过diff数组,可以很容易推回nums的值:nums[i]=nums[i-1]+diff[i];
有了diff数组,对一个区间的所有元素进行加或减就很方便了:想对nums下标i到j区间内元素(包括i和j)全部加5,只要让diff[i]+=5
,再让diff[j+1]-=5
就行,比如i=1,j=3如下:
下标 | 0 | 1(i) | 2 | 3(j) | 4 |
---|---|---|---|---|---|
nums | 4 | 6 | 14 | 12 | 2 |
diff | 4 | -3+5=2 | 8 | -2 | -5-5=-10 |
让diff[i]+=5,相当于让nums[i]开始的元素都加5,再让diff[j+1]-=5,相当于从nums[j+1]开始的元素保持原来大小,这样就相当于对i到j区间元素都加5.
这样每次只要O(1)时间对diff数组修改,就可以完成nums的区间加减,代码如下:
// 差分数组工具类(代码来自于labuladong公众号)
class Difference {
// 差分数组
private int[] diff;
/* 输入一个初始数组,区间操作将在这个数组上进行 */
public Difference(int[] nums) {
assert nums.length > 0;
diff = new int[nums.length];
// 根据初始数组构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
/* 给闭区间 [i, j] 增加 val(可以是负数)*/
public void increment(int i, int j, int val) {
diff[i] += val;
//当 j+1 >= diff.length 时,说明是对 nums[i] 及以后的整个数组都进行修改,那么就不需要再给 diff 数组减 val 了
if (j + 1 < diff.length) {
diff[j + 1] -= val;
}
}
/* 返回结果数组 */
public int[] result() {
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
return res;
}
}
力扣 370. 区间加法
//差分数组,空间O(n),每次操作时间O(1),进行k次,最后生成res时间O(n)
//总时间O(k+n)
class Solution {
public int[] getModifiedArray(int length, int[][] updates) {
//差分数组,原数组初始为0,则差分数组初始也是全0
int[] diff=new int[length];
for(int[] update:updates){
int i=update[0];//索引下标区间[i,j]
int j=update[1];
int m=update[2];//增加的值
diff[i]+=m;
// j+1 >= length 时,说明是对 nums[i] 及以后的整个数组都进行修改,那么就不需要再给 diff 数组减回去了。
if(j+1<length)
diff[j+1]-=m;
}
//结果数组
int[] res=new int[length];
//根据差分数组复原res
res[0]=diff[0];
for(int i=1;i<length;i++){
res[i]=res[i-1]+diff[i];
}
return res;
}
}
力扣 1109. 航班预订统计
原题链接
这道题目翻译一下就是给你一个长度为n的answer数组,初始全为0。再给一个bookings三元组数组,bookings每一个三元组都是[i,j,k],表示让你把nums下标[i-1,j-1]范围内的元素全部加上k(因为数组下标从0开始,航班编号从1开始)。这就是典型的差分数组题。
直接上代码:
//差分数组,时间O(n+k),answer赋值的n,以及k条记录,空间O(1)(如果结果数组不计入空间)
class Solution {
public int[] corpFlightBookings(int[][] bookings, int n) {
int[] diff=new int[n];//差分数组初始为0
for(int[] booking:bookings){
diff[booking[0]-1]+=booking[2];
if(booking[1]<n)
diff[booking[1]]-=booking[2];
}
//结果数组
int[] answer=new int[n];
answer[0]=diff[0];
for(int i=1;i<n;i++){
answer[i]=answer[i-1]+diff[i];
}
return answer;
}
}
其实只需要用一个数组即可:
class Solution {
public int[] corpFlightBookings(int[][] bookings, int n) {
int[] diff=new int[n];//差分数组初始为0
for(int[] booking:bookings){
diff[booking[0]-1]+=booking[2];
if(booking[1]<n)
diff[booking[1]]-=booking[2];
}
//可以不用新建结果数组,是因为题目中原航班量全为0,所以直接用差分数组开始构造即可
for(int i=1;i<n;i++){
diff[i]+=diff[i-1];
}
return diff;
}
}
力扣 1094. 拼车
这题其实是把所有可能的上下车的位置抽象成一个站点数组,数组大小为1001,下标从0到1000;在若干个站点区间内进行区间元素加法(乘客在车上时),乘客下车把加上的元素复原,最后判断每个站点数组元素值是否超过capacity即可,很典型的差分数组类型题:
//差分数组
class Solution {
public boolean carPooling(int[][] trips, int capacity) {
//0到1000,最多有1001个站
int[] diff=new int[1001];
//差分数组赋值
for(int[] trip:trips){
//乘客在trip[1]处上车
diff[trip[1]]+=trip[0];
//trip[2]这个站点乘客下车,我们要计算的是乘客在车上的站点范围[trip[1],trip[2]-1]
//所以从trip[2]减回去
if(trip[2]<1001)//这题里面可以省略,因为题目规定了下车站点最大为1000
diff[trip[2]]-=trip[0];
}
int res=0;
//res记录车上乘客数量
for(int i=0;i<diff.length;i++){
res+=diff[i];
if(res>capacity)return false;
}
return true;//遍历完都符合,说明可以
}
}
力扣 253. 会议室 II
原题链接
这题其实用差分数组效率很低,但是更高效的解法也是建立在差分数组思想上的,所以可以先尝试下差分。
其实就是把所有的会议室的时间抽象成大小为106+1大小的数组,每个使用会议室的时间段ntervals[i] = [starti, endi],就代表这个时间段内会议室的数量需要+1,这个时间段结束后再-1恢复。最后遍历一遍差分数组,比较每个时间点的会议室数量,最大的那个数就是要求的会议室数量。
//差分数组(时空效率很差)
class Solution {
public int minMeetingRooms(int[][] intervals) {
int len=0;
//找到最大的时间
for(int[] interval:intervals){
len=Math.max(len,interval[1]);
}
int[] diff=new int[len];
//构建差分数组
for(int[] interval:intervals){
diff[interval[0]]+=1;
if(interval[1]<len)
diff[interval[1]]-=1;
}
int num=0,res=0;//num记录每个时间点的会议室数量。res记录最大的num
for(int i=0;i<diff.length;i++){
num+=diff[i];
res=Math.max(res,num);
}
return res;
}
}
当然这题有更好的解法,假想将每个使用时间的区间起点和终点都投影到一个线上,区分好每一个区间起点和区间终点,设置一个count计数器记录会议室数量。用一个指针扫描这些起点和终点,只要碰到一个起点,说明有一个会议马上开始,count就要+1,遇到一个终点,说明有一个会议结束,count就-1,这样每个时刻有多少会议就是count的大小。这个扫描过程就是差分数组生成res数组的过程。
//双指针扫描,时间O(nlogn)(排序算法的耗时),空间O(n)
class Solution {
int minMeetingRooms(int[][] meetings) {
int n = meetings.length;
int[] begin = new int[n];
int[] end = new int[n];
for(int i = 0; i < n; i++) {
begin[i] = meetings[i][0];
end[i] = meetings[i][1];
}
Arrays.sort(begin);//将区间起点和终点分别排序
Arrays.sort(end);
// 扫描过程中的计数器
int count = 0;
// 双指针技巧
int res = 0, i = 0, j = 0;
while (i < n && j < n) {
if (begin[i] < end[j]) {
// 扫描到一个起点
count++;
i++;
} else {
// 扫描到一个区间终点
count--;
j++;
}
res = Math.max(res, count);// 记录扫描过程中的最大值
}
return res;
}
}
AcWing 798. 差分矩阵
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.IOException;
public class Main{
static int N=1010;
//对二维差分矩阵进行操作,O(1)
public static void insert(int[][] diff,int x1,int y1,int x2,int y2,int c){
diff[x1][y1]+=c;
diff[x2+1][y1]-=c;
diff[x1][y2+1]-=c;
diff[x2+1][y2+1]+=c;
}
public static void main(String[] args)throws IOException{
//Scanner会超时,所以使用BufferedReader,更快
BufferedReader reader=new BufferedReader(new InputStreamReader(System.in));
BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(System.out));
//将输入的一行字符按去除空格的方式分割进入字符串数组
String[] str1=reader.readLine().split(" ");
int n=Integer.parseInt(str1[0]);//需要将字符转为int类型,可以调用Integer包装类的方法
int m=Integer.parseInt(str1[1]);
int q=Integer.parseInt(str1[2]);
int[][] nums=new int[N][N];//nums矩阵为原矩阵
int[][] diff=new int[N][N];//差分矩阵前缀和就是nums矩阵
//初始化原矩阵与差分矩阵
for(int i=1;i<=n;i++){
//二维矩阵的按行读入str2,下标从0开始
String[] str2=reader.readLine().split(" ");
for(int j=1;j<=m;j++){
nums[i][j]=Integer.parseInt(str2[j-1]);//str2的j从0开始
//初始化二维差分数组(等效于在diff的(i,j)处加上nums[i][j])
insert(diff,i,j,i,j,nums[i][j]);
}
}
//
while(q-->0){
String[] str3=reader.readLine().split(" ");
int x1=Integer.parseInt(str3[0]);
int y1 = Integer.parseInt(str3[1]);
int x2 = Integer.parseInt(str3[2]);
int y2 = Integer.parseInt(str3[3]);
int c = Integer.parseInt(str3[4]);
insert(diff,x1,y1,x2,y2,c);
}
//求二维差分数组的前缀和,就是所求的最终矩阵(原理和二维前缀和数组相同)
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
diff[i][j]+=diff[i-1][j]+diff[i][j-1]-diff[i-1][j-1];
writer.write(diff[i][j]+" ");
}
writer.write("\n");
}
//所有write下的内容,会先存在writers中,当启用flush以后,会输出存在其中的内容。
//如果没有调用flush,则不会将writer中的内容进行输出。
writer.flush();
reader.close();
writer.close();
}
}