差分与前缀和
差分算法的应用:
用于快速处理多次一段区间上的整体加上一个数或者整体减去一个数,通过O(n)的形式得出答案
差分算法的前提基础是前缀和
对于前缀和其实更多时候是对数据的一个预处理操作,让后面通过O(1)获取某个位置的前置累加值
一维差分一维前缀和
核心思想
对于一维前缀和就不作过多介绍,就是遍历是后一个加上前一个迭代就好
对于一位差分来说,我们要整体加上一个数或者整体减去一个数,我们只需要
- 1、在这段区间的起始位置+该数
- 2、在这段区间越界位置-该数
- 3、经过题目多次多个区间整体加减后,我们最后只需使用一维前缀和进行计算就能得出多次相加减后的结果
例题:航班预订统计
题目描述
这里有 n
个航班,它们分别从 1
到 n
进行编号。
有一份航班预订表 bookings
,表中第 i
条预订记录 bookings[i] = [firsti, lasti, seatsi]
意味着在从 firsti
到 lasti
(包含firsti
和 lasti
)的 每个航班 上预订了 seatsi
个座位。
请你返回一个长度为 n
的数组 answer
,里面的元素是每个航班预定的座位总数。
示例 1:
输入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5
输出:[10,55,45,25,25]
解释:
航班编号 1 2 3 4 5
预订记录 1 : 10 10
预订记录 2 : 20 20
预订记录 3 : 25 25 25 25
总座位数: 10 55 45 25 25
因此,answer = [10,55,45,25,25]
示例 2:
输入:bookings = [[1,2,10],[2,2,15]], n = 2
输出:[10,25]
解释:
航班编号 1 2
预订记录 1 : 10 10
预订记录 2 : 15
总座位数: 10 25
因此,answer = [10,25]
解析
典型的多次区间上的整体加整体减操作,差分的典型应用场景
只是这题比较恶心的是,给的航班位置是从1~n
但是要求返回的答案数组下标要求是从0~n-1
也就是说我们在做差分计算时,本来起始位置是from的需要映射为from-1,本来越界位置是to+1的需要映射为to,其中差分题目需要主要越界问题,即要考虑整体加减区间可能包含最后一个位置,那么越界位置就可能造成数组下标越界,所以需要判断一下是否越界
class Solution {
//给的数组含义为
//从booking[i][0]~booking[i][1]区间内预订了booking[i][2]个位置
public int[] corpFlightBookings(int[][] bookings, int n) {
int[] ans = new int[n];
for (int i = 0; i < bookings.length; i++) {
int from = bookings[i][0];
int to = bookings[i][1];
int nums = bookings[i][2];
//差分
ans[from - 1] += nums;
if (to < n) {
ans[to] -= nums;
}
}
//前缀和
for (int i = 1; i < n; i++) {
ans[i] += ans[i - 1];
}
return ans;
}
}
二位差分二维前缀和
二维前缀和思路与构建
目的通过与处理,用O(1)时间内得到区间(a,b)~(c,d)之间的累加和
- 构建二维前缀和从(0,0)到(i,j)
- sum[i][j]+=sum[i-1]+sum[i][j-1]-sum[i-1][j-1]
- 即当前位置(i,j)前缀和=上+下-左上
- 因为在该位置(i,j)时遍历计算时上,下,左上其实就已经算出来了,从而可以计算得到
想要计算从位置(a,b)到位置(c,d)的区间累加和
这个区间的累加和就应该等于:sum(c,b-1)-sum[a-1][b]+sum[a-1][b-1]
304. 二维区域和检索 - 矩阵不可变
题目描述
解析:
其实就是一个标准的二维前缀和能处理的事情
只不过需要注意边界处理,两种方式解决边界
- 方式一:每次获得二维数组或者二维前缀和数组时都判断一下是否越界如果越界那么返回0
- 方式二:多开辟一行一列,就不用进行越界特殊处理(平常最常用也是比较快的)
方式一:
class NumMatrix {
public int[][] sum;
public NumMatrix(int[][] matrix) {
int n=matrix.length;
int m=matrix[0].length;
sum=new int[n][m];
buildSum(matrix);
}
//初始化二维前缀和
private void buildSum(int[][] matrix) {
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[0].length; j++) {
sum[i][j]=get(matrix,i,j)+get(sum,i,j-1)+get(sum,i-1,j)-get(sum,i-1,j-1);
}
}
}
//处理边界,因为这里给的数组下标是从0开始的,我们y牵涉到i-1,j-1操作所以需要处理
public int get(int[][] matrix,int a,int b){
return a>=0&&a<matrix.length&&b>=0&&b<matrix[0].length?matrix[a][b]:0;
}
public int sumRegion(int row1, int col1, int row2, int col2) {
return get(sum,row2,col2)-get(sum,row2,col1-1)-get(sum,row1-1,col2)+get(sum,row1-1,col1-1);
}
}
方式二:
class NumMatrix {
public int[][] sum;
public NumMatrix(int[][] matrix) {
int n=matrix.length;
int m=matrix[0].length;
//多创建一行一列
sum=new int[n+1][m+1];
buildSum(matrix);
}
//初始化二维前缀和
private void buildSum(int[][] matrix) {
for (int i = 1; i <= matrix.length; i++) {
for (int j = 1; j <= matrix[0].length; j++) {
//注意这里sum下标是从1开始matrix下标是从0开始
sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+matrix[i-1][j-1];
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
//由于我们前缀和数组才用的下标是从1开始,而传入的下标是从0开始因此需要映射到前缀和数组
//即应该返回(rowl1+1,col1+1)~(row2+1,cole2+1)
return sum[row2+1][col2+1]-sum[row2+1][col1]-sum[row1][col2+1]+sum[row1][col1];
}
}
最大的以 1 为边界的正方形
题目描述
解析
这个使用二维前缀和就好了,要找边界处全为1的的最大正方形,那么最直接使用二维前缀和的方法,就是首先与处理二维前缀和,然后可以通o(1)获得从区间(a,b)~(c,d)的累加和,之后直接枚举,先枚举每个左上角的点,然后枚举右下角的点(通过最大长度来枚举)找到答案直接返回就好,具体详解看代码中的注释最好理解
class Solution {
//1、预处理二维前缀和
//2、枚举可能的最大正方形长度
//3、找出枚举的当前范围(a,b)~(c~D)的范围累加和
//4、找出大方框内部的方框累加和(a+1,b+1)~(c-1,d-1)
//5、大方框-小方框得到的值是否满足
//返回
public int largest1BorderedSquare(int[][] grid) {
//累加和处理
int n=grid.length;
int m=grid[0].length;
//多创建一行一列用于越界处理
int[][] sum=new int[n+1][m+1];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
// sum[i][j]=grid[i][j]+sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
sum[i][j]= get(grid,i,j)+get(sum,i-1,j)+get(sum,i,j-1)-get(sum,i-1,j-1);
}
}
//提前判断正方形内是否全为0
if (sum[n-1][m-1]==0) return 0;
//枚举最大边长
int len=Math.min(n,m);
for (; len>=1 ; len--) {
//枚举初始位置
for (int i = 0; i+len-1<n; i++) {
for (int j = 0; j+len-1<m; j++) {
//结束位置
int x=i+len-1;
int y=j+len-1;
//(i,j)~(x,y)
int max1=getSum(sum,i,j,x,y);
//内圈:(i+1,j+1)~(x-1,y-1)
int max2=getSum(sum,i+1,j+1,x-1,y-1);
//计算当前最大筐的边界周长
int c=2*len+2*(len-2);
//因为全0的情况已经返回所以这里这样判断是没问题的
if (max1-max2==c)return len*len;
}
}
}
//全为0
return -1;
}
//边界处理,获取
private int get(int[][] grid,int a,int b){
return a>=0&&b>=0&&a<grid.length&&b<grid[0].length?grid[a][b]:0;
}
//计算区间累加和使用的公式以及保证每个区间不越界
private int getSum(int[][] sum ,int i,int j,int x,int y){
return get(sum,x,y)-get(sum,x,j-1)-get(sum,i-1,y)+get(sum,i-1,j-1);
}
}
二维差分
应用场景:
二维差分其实作用和一维差分作用一样的,主要用于区间上的整体加上减去一个树,比如从区间(a,b)~(c,d)统一加上一个数,
基本操作都是先进行差分,然后处理完之后再进行二维累加和就可以了,二维差分对再(a,b)~(c,d)上做的处理是将(a,b)位置加该数然后在(a,d+1)(c+1,b)位置-该数,最后在(c+1,d+1)加这个数就好,最后进行进行前缀和处理就好,具体详解参考以前笔记https://www.yuque.com/wrwang/fqdmqu/yexzq186feppgzc1#DTMhH
地毯
题目描述
解析
public class Main {
public static int MAXN=1010;
public static int MAXM=1010;
public static int[][] map=new int[MAXN][MAXN];
private static int n,m;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(System.out);
in.nextToken(); n=(int)in.nval;
in.nextToken(); m=(int)in.nval;
for (int i = 0; i < m; i++) {
in.nextToken();int a=(int)in.nval;
in.nextToken();int b=(int)in.nval;
in.nextToken();int c=(int)in.nval;
in.nextToken();int d=(int)in.nval;
//构建二维差分
build(a,b,c,d);
}
//计算二维前缀和
getSum();
//输出
for (int i = 1; i <=n; i++) {
for (int j = 1; j <=n; j++) {
out.print(map[i][j]+" ");
}
out.println();
}
out.flush();
out.close();
br.close();
}
private static void getSum() {
for (int i = 1; i <=n; i++) {
for (int j = 1; j <=n; j++) {
map[i][j]+=map[i][j-1]+map[i-1][j]-map[i-1][j-1];
}
}
}
private static void build(int a, int b, int c, int d) {
map[a][b]+=1;
map[a][d+1]-=1;
map[c+1][b]-=1;
map[c+1][d+1]+=1;
}
}
用邮票贴满网格图
题目描述
解析
题目就是给你一个二维数组,其中1表示不能贴邮票的地方,0表示能贴邮票的位置
现在给你一张邮票,邮票是一张stampHeight*stampWidth的,并且每个0位置可以被贴多次
要你判断这张二维数组能不能被贴满
思路:既然题目允许我们每个0位置贴邮票的次数不限制
那么我们就遍历二维数组
然后以当前位置维贴邮票的起点,然后判断这张邮票范围内是否能贴,即使用原来数组求出当前邮票范围内是否==0,如果该为0说明可以贴,否则不可贴
由于我们需要原二维数据的原始数据来判断是否可贴,因此我们不能修改原数据,所以我们需要另开辟一个二维数组用于计算区间和
同时这个开辟的前缀和计算后我们也不能改变,因为他就是原二维数组的映射关系,我们在判断能贴邮票后,
需要给整个区间进行+1操作,这个刚好满足差分的性质,所以我们再开辟一个差分数组差分数组
然后满足贴邮票条件就基于差分数组进行修改最后如果差分数组没有0了就说明能贴满否则不能贴满
!!!注意不能给差分数组用愿数组初始化因为原来数组中不能贴的位置是1,那么如果使用了进行初始化,那么这个位置后面的位置最后进行累加和的时候就会加进去,导致可能后面的位置本来是0的最后因为这个位置变成了1
class Solution {
//题目就是给你一个二维数组,其中1表示不能贴邮票的地方,0表示能贴邮票的位置
//现在给你一张邮票,邮票是一张stampHeight*stampWidth的,并且每个0位置可以被贴多次
//要你判断这张二维数组能不能被贴满
//思路:既然题目允许我们每个0位置贴邮票的次数不限制
//那么我们就遍历二维数组
//然后以当前位置维贴邮票的起点,然后判断这张邮票范围内是否能贴,即使用原来数组求出当前邮票范围内是否==0,如果该为0说明可以贴,否则不可贴
//由于我们需要原二维数据的原始数据来判断是否可贴,因此我们不能修改原数据,所以我们需要另开辟一个二维数组用于计算区间和
//同时这个开辟的前缀和计算后我们也不能改变,因为他就是原二维数组的映射关系,我们在判断能贴邮票后,需要给整个区间进行+1操作,这个
//刚好满足差分的性质,所以我们再开辟一个差分数组差分数组(注意这个差分数组要保持比较存粹的效果,即指用来记录我们贴邮票后的结果)
//然后满足贴邮票条件就基于差分数组进行修改
//最后如果差分数组没有0了就说明能贴满否则不能贴满
int[][] sum;
int[][] diff;
public boolean possibleToStamp(int[][] grid, int stampHeight, int stampWidth) {
int n = grid.length;
int m = grid[0].length;
//前缀和数组和差分数组都从1开始
build(grid);
//计算前缀和
for (int a = 1,c=a+stampHeight-1; c<=n ; a++,c++) {
for (int b = 1,d=b+stampWidth-1; d<=m; b++,d++) {
if (getRangSum(a,b,c,d)==0){
//差分
buidDiff(a,b,c,d);
}
}
}
//差分完后,计算差分数组,看看得到差分后的值
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
diff[i][j] +=diff[i][j-1]+diff[i-1][j]-diff[i-1][j-1];
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (diff[i][j] == 0 && grid[i - 1][j - 1] == 0) {
return false;
}
}
}
return true;
}
private void build(int[][] grid) {
int n = grid.length;
int m = grid[0].length;
sum = new int[n + 1][m + 1];
diff = new int[n + 2][m + 2];
//初始化前缀和数组和差分数组
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
sum[i][j] = grid[i - 1][j - 1]+sum[i][j-1]+sum[i-1][j]-sum[i-1][j-1];
}
}
}
private int getRangSum(int a,int b,int c,int d){
//因为我们前缀和是从1开始的,而原始数组下标是从0开始的,所以需要映射
//(a,b)~(c,d)
return sum[c][d]-sum[c][b-1]-sum[a-1][d]+sum[a-1][b-1];
}
private void buidDiff(int a,int b,int c,int d){
//(a,b)~(c,d)
diff[a][b]+=1;
diff[a][d+1]-=1;
diff[c+1][b]-=1;
diff[c+1][d+1]+=1;
}
}