对于查询区间和的问题,可以预处理出来一个前缀和数组 dp,数组中存储的是从下标 0 的位置到当前位置的区间和,这样只需要通过前缀和数组就可以快速的求出指定区间的和了,例如求 l ~ r 区间的和,就可以之间使用 dp[l - 1] - dp[r] 来计算
1. DP34 【模板】前缀和
这里从下标 1 开始填是为了在初始化前缀和数组时更方便
public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int N = sc.nextInt(); int p = sc.nextInt(); int[] arr = new int[N + 1]; long[] dp = new long[N + 1]; for (int i = 1; i <= N; i++) { arr[i] = sc.nextInt(); } for (int i = 1; i <= N; i++) { dp[i] = dp[i - 1] + arr[i]; } int l = 0, r = 0; while (p-- != 0) { l = sc.nextInt(); r = sc.nextInt(); System.out.println(dp[r] - dp[l - 1]); } } }
2. DP35 【模板】二维前缀和
和一维的前缀和数组类似,这里需要先预处理出来一个前缀和矩阵 dp[][],dp[i][j] 就表示从(1,1)到(i,j)这个矩阵中的所有元素的和
放到矩阵中可以看出,如果想要求(1,1)到(i,j)区间内的区域和,需要先加上 A,B,C,D 四个区域的和,如果单独的表示 B 区域或者 C 并不好表示,但是 A + B 和 A + C 是很好表示的,把这两个区域加起来再减去多加的 A ,再加上 D 就是整个区域的和
得到了前缀和数组之后,该怎么去使用呢?
如果说给出了(x1,y1)(x2,y2)两个点,那么就是求红色框的元素的和
也就是求出 D 区域的和,由于 B 和 C 并不好单独转换,就可以转化为 A+B+C+D 的值先减去 A+B 的值,再减去 A + C 的值,此时方法 A 被多减了一次,再加上就是 D 区域的和了
public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(), m = sc.nextInt(); int q = sc.nextInt(); int[][] A = new int[n + 1][m + 1]; long[][] dp = new long[n + 1][m + 1]; 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++) { dp[i][j] = dp[i][j - 1] + dp[i - 1][j] - dp[i - 1][j - 1]+ A[i][j]; } } int x1 = 0,y1 = 0,x2 = 0,y2 = 0; while (q-- != 0) { x1 = sc.nextInt(); y1 = sc.nextInt(); x2 = sc.nextInt(); y2 = sc.nextInt(); System.out.println(dp[x2][y2] - dp[x2][y1 - 1] - dp[x1 - 1][y2] + dp[x1 - 1][y1 - 1]); } } }
3. 寻找数组的中心下标
根据题目要求就是求 0~ i - 1区间的和是否和区间 i + 1~n - 1 的和相等,那么此题dp[i] 就表示的是[0,i-1] 区间的和而不是之前的 0 ~ i 区间的和了,相应的,状态转移方程也要有所变化
同时这道题还需要求一个后缀和数组用来描述后半段的区间和,也是同样的道理,只不过 dp[i] 表示的是[i + 1,n - 1] 区间的和,这段区间的状态转移方程也就是 dp[i+1] + nums[i + 1]
class Solution { public int pivotIndex(int[] nums) { int n = nums.length; int[] dp = new int[n]; int[] g = new int[n]; for (int i = 1; i < n; i++) { dp[i] = dp[i - 1] + nums[i - 1]; } for (int i = n - 2; i >= 0; i--) { g[i] = g[i + 1] + nums[i + 1]; } for (int i = 0; i < n; i++) { if (g[i] == dp[i]) { return i; } } return -1; } }
4. 除自身以外数组的乘积
这道题其实就和上道题非常类似,把 i 位置之前的数都求一个前缀积,之后的数求一个后缀积,只需要把前缀积和后缀积相乘就可以了
class Solution { public int[] productExceptSelf(int[] nums) { int n = nums.length; int[] f = new int[n]; int[] g = new int[n]; int[] ans = new int[n]; f[0] = 1; g[n - 1] = 1; for (int i = 1; i < n; i++) { f[i] = f[i - 1] * nums[i - 1]; } for (int i = n - 2; i >= 0; i--) { g[i] = g[i + 1] * nums[i + 1]; } for (int i = 0; i < n; i++) { ans[i] = f[i] * g[i]; } return ans; } }
5. 和为 K 的子数组
思路:每次都求0 ~ i - 1 区间内有多少个子数组是和为 k 的,如果正常求的话,时间复杂度就是O(n^2)了,所以说可以把子数组和为k的个数存在哈希表中去求,也就需要在求和的过程中就把这些数据添加到哈希表中,而求0 ~ i - 1 区间,和为 k 的子数组,就可以转化为求前半部分的哪段区间的和为整段区间和 sum - k
注意点:
- 不用真正的创建一个前缀和数组去表示和,只需要用一个 sum 变量来计算即可
- 如果说整个前缀和都等于k的话,就代表 sum - k 等于 0,这个需要提前在哈希表中存储,因为此时需要在 0~-1 区间内去找一个和为0的次数,但是这个区间不存在,所以说需要提前设置为 1,当需要查找的时候,就是默认的 1
- 前缀和加入到哈希表的时机:需要在计算i位置之前,保存0~i-1区间的前缀和,也就是知道 sum - k的次数,i-1统计之后才可以把i位置的前缀和存入哈希表中
class Solution { public int subarraySum(int[] nums, int k) { int sum = 0,ret = 0; HashMap<Integer,Integer> hash = new HashMap<>(); hash.put(0,1); for(int x : nums){ sum+=x; //以当前元素为结尾时有多少符合条件的答案 ret+=hash.getOrDefault(sum - k,0); hash.put(sum,hash.getOrDefault(sum,0) + 1); } return ret; } }
6. 和可被 K 整除的子数组
同余定理:(a - b) / p = k......0 => a % p = b % p
就是如果 a - b 的差能够被 p 整除的话,那么 a 和 b 对 p 取模就相等
在 Java 中,一个负数对一个正数取模的话得到的是一个负数,如果想要修正为正数的话可以使用下面的式子
首先把负数的余数变为正数,但如果原来就是正数的话还是不对的,所以需要再取一个余数
这样就可以把题目转化为只需要求在 i 之前有多少个区间对 K 取模之后等于 0 ,也就和上一题类似了
class Solution { public int subarraysDivByK(int[] nums, int k) { int sum = 0, ret = 0; HashMap<Integer, Integer> hashMap = new HashMap<>(); hashMap.put(0 % k, 1); for (int x : nums) { sum += x; int r = (sum % k + k)% k; ret += hashMap.getOrDefault( r, 0); hashMap.put(r, hashMap.getOrDefault(r, 0) + 1); } return ret; } }
7. 连续数组
如果直接统计 0 和 1 的个数的话还是比较麻烦的,可以转化一下,把所有的 0 都变成 - 1,当 0 和 1 的个数相等时,也就是区间和等于 0 的情况,也就转化为了之前的求和为 k 的子数组的问题,只不过之前求的是个数,这题求的是长度,
class Solution { public int findMaxLength(int[] nums) { HashMap<Integer, Integer> hash = new HashMap<>(); hash.put(0, -1); int sum = 0, ret = 0; for (int i = 0; i < nums.length; i++) { sum += (nums[i] == 0 ? -1 : 1); if (hash.containsKey(sum)) { ret = Math.max(ret, i - hash.get(sum)); } else { hash.put(sum, i); } } return ret; } }
需要注意的是,如果说在之后发现有重复的 sum 的时候,就不需要再存放进哈希表中了,因为此时的长度肯定是没有第一次的长的,就会影响后面使用 i - j 时计算的长度
8. 矩阵区域和
也就是周围所有元素的和
首先就是先预处理一个二维前缀和数组,然后再求( i , j ) 位置的值
求(i , j )位置的值的时候和之前讲的前缀和模版类似
然后就是怎么求坐标的问题,知道(i , j)坐标之后,求此处的值的话就需要求左上角和右下角的坐标,然后才能求出这个区域中的元素和
关于下标的映射关系,由于题目中的数组是从下标 0 计算的,为了方便是将 dp 表从下标为 1 开始计算的,所以说后续再继续从 dp 表中使用值的时候是需要把 x ,y 坐标都加上 1 的
class Solution { public int[][] matrixBlockSum(int[][] mat, int k) { int m = mat.length, n = mat[0].length; int[][] dp = new int[m + 1][n + 1]; int[][] ans = new int[m][n]; for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { dp[i][j] = dp[i - 1][j] + dp[i][j-1] - dp[i - 1][j - 1] + mat[i - 1][j - 1]; } } for(int i = 0; i< m;i++){ for(int j = 0;j < n;j++){ int x1 = Math.max(0,i - k) + 1,y1 = Math.max(0,j - k) + 1; int x2 = Math.min(m - 1,i + k) + 1,y2 = Math.min(n - 1, j + k) + 1; ans[i][j] = dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1]; } } return ans; } }