【模板】前缀和
题目链接:前缀和
算法思路
先预处理出来⼀个「前缀和」数组:
- ⽤ dp[i] 表⽰: [1, i] 区间内所有元素的和,那么 dp[i - 1] ⾥⾯存的就是 [1, i - 1] 区间内所有元素的和,那么:可得递推公式: dp[i] = dp[i - 1] + arr[i] ;
- 使⽤前缀和数组,「快速」求出「某⼀个区间内」所有元素的和: 当询问的区间是 [l, r] 时:区间内所有元素的和为: dp[r] - dp[l - 1] 。
代码
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
while (scan.hasNextInt()) {
int n = scan.nextInt();
int q = scan.nextInt();
int[] array = new int[n + 1];
for(int i = 1; i <= n; i++) {
array[i] = scan.nextInt();
}
// 使用long防止溢出
long dp[] = new long[n + 1];
dp[0] = 0; // 初始化
for(int i = 1; i <= n; i++) {
// 前缀和
dp[i] = dp[i - 1] + array[i];
}
for(int i = 0; i < q; i++) {
int l = scan.nextInt();
int r = scan.nextInt();
// 使用前缀和数组
System.out.println(dp[r] - dp[l - 1]);
}
}
}
}
【模板】二维前缀和
题目链接:【模板】二维前缀和
算法思路:
代码:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
int q = scan.nextInt();
int[][] array = new int[n+1][m+1];
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
array[i][j] = scan.nextInt();
}
}
// 计算前缀和
long[][] dp = new long[n+1][m+1];
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] + array[i][j];
}
}
while(q > 0) {
int x1 = scan.nextInt();
int y1 = scan.nextInt();
int x2 = scan.nextInt();
int y2 = scan.nextInt();
// 使用前缀和
System.out.println(dp[x2][y2] - dp[x2][y1 - 1] - dp[x1 - 1][y2] + dp[x1-1][y1-1]);
q--;
}
}
}
寻找数组的中心下标
题目链接:寻找数组的中心下标
算法思路:
代码:
class Solution {
public int pivotIndex(int[] nums) {
int n = nums.length;
int[] ldp = new int[n+1];
int[] rdp = new int[n+1];
// 计算左前缀和
for(int i = 1; i < n; i++) {
// ldp[i]计算的是[0,i-1]之和
ldp[i] = ldp[i - 1] + nums[i - 1];
}
// 计算右前缀和
for(int i = n - 2; i >= 0; i--) {
// rdp[i]计算的是[i+1, n-1]之和
rdp[i] = rdp[i+1] + nums[i+1];
}
for(int i = 0; i < n; i++) {
if(ldp[i] == rdp[i]) {
return i;
}
}
return -1;
}
}
除自身以外数组的乘积
题目链接:除自身以外数组的乘积
算法思路:
代码:
class Solution {
public int[] productExceptSelf(int[] nums) {
// 题目要求不能使用除法
// 可以前缀积*后缀积
int n = nums.length;
int[] ldp = new int[n+1];
int[] rdp = new int[n+1];
// 乘数不能为0
ldp[0] = 1;
rdp[n-1] = 1;
// 求前缀积
for(int i = 1; i < n; i++) {
// ldp[i]等于[0, i-1]之间的乘积
ldp[i] = ldp[i-1] * nums[i-1];
}
// 求后缀积
for(int i = n - 2; i >= 0; i--) {
// rdp[i]等于[i+1, n-1]之间的乘积
rdp[i] = rdp[i+1] * nums[i+1];
}
int[] answer = new int[n];
for(int i = 0; i < n; i++) {
answer[i] = ldp[i] * rdp[i];
}
return answer;
}
}
和为K的子数组
题目链接:和为k的子数组
算法思路:
代码:
class Solution {
public int subarraySum(int[] nums, int k) {
// 我的第一个想法是滑动窗口,但是不行,因为数组中有0和负数,不具有单调性。
// 我们可以考虑前缀和
// 求该数组中和为 k 的子数组的个数,即求sum - k有几个
// 所以可以引入哈希表,统计前缀和的个数
Map<Integer, Integer> hashMap = new HashMap<>();
// 如果整个前缀和为k时
hashMap.put(0, 1);
int sum = 0;// 用来统计当前位置的前缀和
int result = 0; // 用来统计个数
for(int x : nums) {
sum += x;
// 更新结果
result += hashMap.getOrDefault(sum - k, 0);
// 把当前前缀和加入哈希表
hashMap.put(sum, hashMap.getOrDefault(sum, 0) + 1);
}
return result;
}
}
和可被K整除的子数组
题目链接:和可被K整除的子数组
算法思路:
设 i 为数组中的任意位置,⽤ sum[i] 表⽰ [0, i] 区间内所有元素的和。
- 想知道有多少个「以 i 为结尾的可被 k 整除的⼦数组」,就要找到有多少个起始位置为 x1, x2, x3… 使得 [x, i] 区间内的所有元素的和可被 k 整除。
- 设 [0, x - 1] 区间内所有元素之和等于 a , [0, i] 区间内所有元素的和等于 b ,可得 (b - a) % k == 0 。
- 由同余定理可得, [0, x - 1] 区间与 [0, i] 区间内的前缀和同余。于是问题就变成: 找到在 [0, i - 1] 区间内,有多少前缀和的余数等于 sum[i] % k 的即可。
我们不⽤真的初始化⼀个前缀和数组,因为我们只关⼼在 i 位置之前,有多少个前缀和等于 sum[i] - k 。因此,我们仅需⽤⼀个哈希表,⼀边求当前位置的前缀和,⼀边存下之前每⼀种前缀和出现的次数。
代码:
class Solution {
public int subarraysDivByK(int[] nums, int k) {
Map<Integer,Integer> hashMap = new HashMap<>();
// 整个前缀和为0
hashMap.put(0%k,1);
int sum = 0; // 求前缀和
int result = 0;
for(int x : nums) {
sum += x; // 求当前位置的前缀和
int r = (sum % k + k) % k; // 求余数
// 更新结果
result += hashMap.getOrDefault(r, 0);
hashMap.put(r, hashMap.getOrDefault(r, 0) + 1);
}
return result;
}
}
连续数组
题目链接:连续数组
算法思想:
设 i 为数组中的任意位置,⽤ sum[i] 表⽰ [0, i] 区间内所有元素的和。
想知道最⼤的「以 i 为结尾的和为 0 的⼦数组」,就要找到从左往右第⼀个 x1 使得 [x1,i] 区间内的所有元素的和为 0 。那么 [0, x1 - 1] 区间内的和是不是就是 sum[i] 了。于是问题就变成:
- 找到在 [0, i - 1] 区间内,第⼀次出现 sum[i] 的位置即可。 我们不⽤真的初始化⼀个前缀和数组,因为我们只关⼼在 i 位置之前,第⼀个前缀和等于 sum[i] 的位置。因此,我们仅需⽤⼀个哈希表,⼀边求当前位置的前缀和,⼀边记录第⼀次出现该前缀和的位置。
代码:
class Solution {
public int findMaxLength(int[] nums) {
// 将0换成-1,即求和为0的最长连续子数组
int n = nums.length;
for(int i = 0; i < n; i++) {
if(nums[i] == 0) {
nums[i] = -1;
}
}
Map<Integer, Integer> hash = new HashMap<>();
hash.put(0, -1); // 前缀和为0
int sum = 0;
int result = 0;
for(int i = 0; i < n; i++) {
sum += nums[i];
if(hash.containsKey(sum)) {
result = Math.max(result, i - hash.get(sum));
}else {
// 第一次出现
hash.put(sum, i);
}
}
return result;
}
}
矩阵区域和
题目链接:https://leetcode.cn/problems/matrix-block-sum/
算法思路:
⼆维前缀和的简单应⽤题,关键就是我们在填写结果矩阵的时候,要找到原矩阵对应区域的「左上⻆」以及「右下⻆」的坐标(推荐⼤家画图)左上⻆坐标: x1 = i - k,y1 = j - k ,但是由于会「超过矩阵」的范围,因此需要对 0取⼀个 max 。因此修正后的坐标为: x1 = max(0, i - k), y1 = max(0, j - k) ;右下⻆坐标: x1 = i + k,y1 = j + k ,但是由于会「超过矩阵」的范围,因此需要对 m-1 ,以及 n - 1 取⼀个 min 。因此修正后的坐标为: x2 =min(m - 1, i + k), y2 = min(n - 1, j + k)
然后将求出来的坐标代⼊到「⼆维前缀和矩阵」的计算公式上即可~(但是要注意下标的映射关系)
代码:
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];
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];
}
}
// 使用二维前缀和
int[][] answer = new int[m][n];
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;
answer[i][j] = dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1];
}
}
return answer;
}
}