二维前缀和详解
介绍
这几天在打比赛时遇到了二维前缀和,看了一下深有体会,发一篇详解。
首先,什么是前缀和?一个数列,我们要计算某个区间内的和,该怎么做呢?正所谓暴力出奇迹,这一个也可以,我们暴力枚举每一个区间内的数并且相加,可是这个是O(n)的时间复杂度,不要小看这个线性,可如果在DP里的话这相当于加了一次方,卡你非常简单,下面讲一下O(n)的预处理,O(1)的计算,你可能会说这个也是线性,有差吗?当然有差,这个是需处理,不会像暴力一样在DP中使DP的时间复杂度加一次方而是直接加上O(n)。下面讲一下做法,前缀和,顾名思义就是第几个数之前的数的和,我们用DP来预处理,我们定义状态DP[i]表示到第i个数(包括它)为止前面所有数的和,从而得出状态转移方程DP[i]=DP[i-1]+num[i],num[i]表示数组中第i个数,这样要计算闭区间i,j(闭区间就是指i<=x<=j,x就是区间里的数)的和就是DP[j]-DP[i-1],这个画图理解如下:
下面我们讲一下什么是二维前缀和,建立在一维前缀和之上,我们要求一个矩阵内一个任意的子矩阵的数的和,我们就可以用二维前缀和,我们还是用DP来预处理,状态和一维前缀和差不多,只不过我们多加了一维,DP[i][j]表示(1,1)这个点与(i,j)这个点两个点分别为左上角和右下角所组成的矩阵内的数的和,好好想一下状态转移方程,DP[i][j]=DP[i-1][j]+DP[i][j-1]-DP[i-1][j-1]+map[i][j],怎么来的呢?我们画一下图就知道了。
这张图就知道了(i,j)可以由(i-1,j)和(i,j-1)两块构成,不过要注意两个点,1、有一块矩阵我们重复加了,也就是(i-1,j-1)这一块,所以我们要减去它。2、我们这个矩阵是不完整的,由图可知我们还有一块深蓝色的没有加,也就是(i,j)这一点,所以我们要再加上map[i][j]也就是题目给出的矩阵中这一格的数。这样我们就预处理完了,现在讲一下怎么通过我们的预处理从而快速地得出我们想要的任意子矩阵中的和,我们定义(x1,y1)为我们想要子矩阵的左上角,(x2,y2)为我们想要子矩阵的右下角,然后我们画图想一想。
我们可以通过DP[x2][y2]来计算,我们通过图可以发现这个距离我们要的还差红色的部分看看怎么表示红色部分?我们可以分割成两块,分别是DP[x1][y2]和DP[x2][y1]我们发现有一块重复减了,所以我们再加上它即DP[x1][y1],有一点注意,因为画图和定义原因我们发现边界好像不对,我们来看看,我们定义的状态是整个矩阵包括边的和,而我们要求的也是要包括边的,所以我们要再改一下,把DP[x1][y2]和DP[x2][y1]和DP[x1][y1]分别改成DP[x1-1][y2]和DP[x2][y1-1]和DP[x1-1][y1-1]这样一减我们就可以得到自己想要的答案,整理可得公式,DP[x2][y2]-DP[x1-1][y2]-DP[x2][y1-1]+DP[x1-1][y1-1] 这样我们就可以做到O(1)之内查询,很奇妙吧,我们看一下实现代码:
1 package com.lzp.util.algorithm; 2 3 import java.util.Scanner; 4 5 /** 6 * @Author LZP 7 * @Date 2021/4/22 21:19 8 * @Version 1.0 9 * 10 * 二位前缀和(Two dimensional prefix Sum) 11 * 求解任意一个子矩阵的和 12 */ 13 public class TDPS { 14 public static void main(String[] args) { 15 Scanner input = new Scanner(System.in); 16 int n = input.nextInt(); 17 int m = input.nextInt(); 18 19 int[][] prefix = new int[n + 1][m + 1]; 20 21 // 二维前缀和 22 for (int i = 1; i <= n; i++) { 23 for (int j = 1; j <= m; j++) { 24 prefix[i][j] = prefix[i - 1][j] + prefix[i][j - 1] - prefix[i - 1][j - 1] + input.nextInt(); 25 } 26 } 27 28 // 求任意一个矩阵内一个任意的子矩阵的数的和,并返回最大的那个子矩阵的和(每个矩阵位置数的范围是[-10000, 10000]) 29 // 枚举四个边界,分别是x1、x2、y1、y2 30 // 时间复杂度:O((nm) ^ 2) 31 int max = Integer.MIN_VALUE; 32 for (int x1 = 1; x1 <= n; x1++) { 33 for (int x2 = x1; x2 <= n; x2++) { 34 for (int y1 = 1; y1 <= m; y1++) { 35 for (int y2 = y1; y2 <= m; y2++) { 36 int sum = prefix[x2][y2] - prefix[x1 - 1][y2] - prefix[x2][y1 - 1] + prefix[x1 - 1][y1 - 1]; 37 max = Math.max(max, sum); 38 } 39 } 40 } 41 } 42 43 // 输出子矩阵和中最大的和 44 System.out.println(max); 45 46 // k次查询 47 int k = input.nextInt(); 48 for (int i = 0; i < k; i++) { 49 // 查询任意一个子矩阵的和 50 int x1 = input.nextInt(); 51 int y1 = input.nextInt(); 52 int x2 = input.nextInt(); 53 int y2 = input.nextInt(); 54 System.out.println(prefix[x2][y2] - prefix[x1 - 1][y2] - prefix[x2][y1 - 1] + prefix[x1 - 1][y1 - 1]); 55 } 56 } 57 }
运行结果
力扣题:363. 矩形区域不超过 K 的最大数值和
题目描述
给你一个 m x n 的矩阵 matrix 和一个整数 k ,找出并返回矩阵内部矩形区域的不超过 k 的最大数值和。
题目数据保证总会存在一个数值和不超过 k 的矩形区域。
示例 1:
输入:matrix = [[1,0,1],[0,-2,3]], k = 2
输出:2
解释:蓝色边框圈出来的矩形区域 [[0, 1], [-2, 3]] 的数值和是 2,且 2 是不超过 k 的最大数字(k = 2)。
示例 2:
输入:matrix = [[2,2,-1]], k = 3
输出:3
提示:
m == matrix.length
n == matrix[i].length
1 <= m, n <= 100
-100 <= matrix[i][j] <= 100
-105 <= k <= 105
思路
这题的标签:
前缀和 二分
这题可以通过预处理将二维前缀和先算出来,然后枚举三个边界(其实暴力是要枚举四个边界的,不过这题如果直接纯暴力会超时,所以我们就要将其中一个n优化为log(n)),这里的优化可以使用Java中提供的TreeSet集合中的ceiling()方法来实现,ceiling()方法的底层是用二分来实现的,所以这题刚好可以用上。之前没有用过TreeSet中的ceiling()和floor()这两个方法,还是因为这题才学会使用的,通过这题在当时也是听了y总的思路才弄懂了一点,不然以我没有系统学过算法的人来说肯定是很难想出来的,最近自己也在慢慢研究算法,因为算法可以培养自己的逻辑思维能力,对以后做软件开发也是很有帮助的,希望有志同道合的人能一起学习一起进步。(算法很美)
二刷经验
提问:为啥set集合一开始要加入0这个数?
解答:
AC代码
1 class Solution { 2 public int maxSumSubmatrix(int[][] matrix, int k) { 3 int n = matrix.length; 4 int m = matrix[0].length; 5 6 int[][] f = new int[n + 1][m + 1]; 7 8 // 前缀和 9 for (int i = 1; i <= n; i++) { 10 for (int j = 1; j <= m; j++) { 11 f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + matrix[i - 1][j - 1]; 12 } 13 } 14 15 16 int res = Integer.MIN_VALUE; 17 for (int x1 = 1; x1 <= n; x1++) { 18 for (int x2 = x1; x2 <= n; x2++) { 19 TreeSet<Integer> set = new TreeSet<>(); 20 set.add(0); 21 for (int y2 = 1; y2 <= m; y2++) { 22 int si = get(x1, 1, x2, y2, f); 23 Integer a = set.ceiling(si - k); 24 if (a != null) { 25 // 更新 26 res = Math.max(res, si - a); 27 } 28 // 加入set集合 29 set.add(si); 30 } 31 } 32 } 33 34 return res; 35 } 36 37 public int get(int x1, int y1, int x2, int y2, int[][] f) { 38 return f[x2][y2] + f[x1 - 1][y1 - 1] - f[x1 - 1][y2] - f[x2][y1 - 1]; 39 } 40 }
leetcode链接:https://leetcode-cn.com/problems/max-sum-of-rectangle-no-larger-than-k/