当前位置:首页 » 《关于电脑》 » 正文

算法【Java】 —— 前缀和

1 人参与  2024年09月10日 14:02  分类 : 《关于电脑》  评论

点击全文阅读


模板引入

一维前缀和

https://www.nowcoder.com/share/jump/9257752291725692504394

在这里插入图片描述


解法一:暴力枚举

在每次提供 l 与 r 的时候,都从 l 开始遍历数组,直到遇到 r 停止,这个方法的时间复杂度为 O(N * q)

解法二:前缀和

如果我们事先就知道所有从 0 开始的子数组的前缀和的话,那么要计算 从 l 到 r 之间的字数组的和的时候,我们就可以利用这个已知的前缀和数组进行计算,也就是 ans = dp[r] - dp[l] + nums[l]

时间复杂度为 O(N + q) , 空间复杂度为 O(N)

import java.util.Scanner;// 注意类名必须为 Main, 不要有任何 package xxx 信息public class Main {    public static void main(String[] args) {        Scanner in = new Scanner(System.in);                int n = in.nextInt();        int q = in.nextInt();        long[] arr = new long[n];        for(int i = 0; i < n; i++) {            arr[i] = in.nextInt();        }        //构建前缀和数组        long[] dp = new long[n+1];        for(int i = 1; i <= n; i++) {            dp[i] = dp[i-1] + arr[i-1];        }        while(q > 0) {            int l = in.nextInt();            int r = in.nextInt();            System.out.println(dp[r] - dp[l] + arr[l-1]);            q--;        }    }}

二维前缀和

https://www.nowcoder.com/share/jump/9257752291725693083065

在这里插入图片描述
在这里插入图片描述


解法一:暴力枚举

直接遍历矩阵,时间复杂度为 O(N² * q)

解法二:前缀和

和上面那一道题目一样,我们先预处理一个前缀和数组,只不过这次是一个二维数组,我们需要讨论一下:

在这里插入图片描述

由上图可知,要想求 dp[i][j],我们可以利用上面的关系来推导,也就是 dp[i][j] 等于三块颜色的区域 加上 原矩阵对应的 nums[i][j]

dp[i-1][j] 等于蓝色区域加上绿色区域,dp[i][j-1] 等于蓝色区域加上橙色区域,dp[i-1][j-1] 等于蓝色区域

dp[i][j] = dp[i-1][j] + dp[i][j-1] - dp[i-1][j-1] + nums[i][j]

但是要注意可能会出现数组越界访问的情况,因为当 i = 0 或者 j = 0 的时候 ,dp[i][j-1] 是会发生越界访问的,所以要避免发生越界访问的话,我们可以加多一个外围区域,就是将 dp 数组 在创建的时候,可以将 dp[][] = new int[n+1][m+1],也就是比原先数组多一行和多一列

这样的话,我们在进行 dp 数组计算的时候,下标就应该从 1 开始,对应的nums 数组的下标则是减一即可。

dp[i][j] = dp[i-1][j] + dp[i][j-1] - dp[i-1][j-1] + nums[i-1][j-1]

如何获取从 (x1,y1) 到 (x2,y2) 之间的区域的和

在这里插入图片描述
因为正好是x1 ,y1, x2, y2 可以对应我们的 dp 数组,所以直接使用

蓝色区域是我们要求的区域,蓝色区域 等于 = dp[x2][y2] - (dp[x1-1][y2] + dp[x2][y1-1] - dp[x1-1][y1-1]

import java.util.Scanner;// 注意类名必须为 Main, 不要有任何 package xxx 信息public class Main {    public static void main(String[] args) {        Scanner in = new Scanner(System.in);                int n = in.nextInt();        int m = in.nextInt();        int q = in.nextInt();        int[][] arr = new int[n][m];        for(int i = 0; i < n; i++) {            for(int j = 0; j < m; j++) {                arr[i][j] = in.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] + arr[i-1][j-1];            }        }        while(q > 0) {            int x1 = in.nextInt();            int y1 = in.nextInt();            int x2 = in.nextInt();            int y2 = in.nextInt();            System.out.println(dp[x2][y2] - (dp[x1-1][y2] + dp[x2][y1-1] - dp[x1-1][y1-1]));            q--;        }    }}

小结

前缀和主要用于处理数组区间的和的问题。

前缀和处理二维数组(矩阵)的时候,要注意 dp 数组要扩大。

实战演练

寻找数组的中心下标

https://leetcode.cn/problems/find-pivot-index/description/

在这里插入图片描述

解析:
中心下标表示除了这个下标对应的数字之外,其左边区域和等于右边区域和

区域和,我们可以联想到前缀和来解决,由于需要比较左右两边的区域和,我们可以设置前缀和数组与后缀和数组。

注意事项
前缀和应该从 下标 1 开始计算,f[i] = f[i-1] + nums[i-1]
后缀和应该从下标 n - 2 开始计算,g[i] = g[i+1] + nums[i+1]
因为上面两条公式需要利用到之前的前缀和或者后缀和,如果前缀和从0开始计算,则会数组越界访问异常,并且题目告诉我们如果中心下标是最左端,则左侧数组和为 0 ,右侧也一样,所以我们可以直接从 0 或者 n - 2 开始计算。

class Solution {    public int pivotIndex(int[] nums) {        int n = nums.length;        int[] f = new int[n];        int[] g = new int[n];        //构建前缀和数组        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++) {            if(f[i] == g[i])                return i;        }        return -1;    }}

除自身以外数组的乘积

https://leetcode.cn/problems/product-of-array-except-self/description/

在这里插入图片描述

解析:
和上面的题目类似,这次是乘法,我们可以使用前缀积和后缀积来解答

这里要注意 f[0] 和 g[n-1] 要先预设置为 0,避免后续计算的时候得到的全是 0

class Solution {    public int[] productExceptSelf(int[] nums) {        int n = nums.length;        int[] f = new int[n];        int[] g = 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];        }        int[] ans = new int[n];        for(int i = 0; i < n; i++) {            ans[i] = f[i] * g[i];        }        return ans;    }}

和为 K 的子数组

https://leetcode.cn/problems/subarray-sum-equals-k/description/

在这里插入图片描述


注意这里不能使用滑动窗口来解决!!!

滑动窗口有一个特点就是右指针不能进行回退,所以当你要使用滑动窗口来解决子数组的时候,要保证一个前提:数组具有单调性。

而这道题目由于数组可能会出现负数,也就是说明数组是不具有单调性的,举个例子:
在这里插入图片描述

两块蓝色区域相加正好等于零,而中间红色区域等于K 的时候,你使用滑动窗口,由于右指针不能回退,导致遗漏掉中间红色数组的情况,所以,我们不能使用滑动窗口。


解法一:暴力枚举

通过两层 循环,获取所有的子数组,时间复杂度为 O(N²)

解法二:前缀和加哈希表

计算 [0, i] 区间的前缀和 sum[i]

我们把要求的 K 子数组定义为以 i 结尾的子数组

如果存在 K 的子数组的话:
在这里插入图片描述

那么如果 i 数组前面出现过 子数组和为 sum[j] 的话,就说明存在 K 数组,sum[j] = sum[i] - K

我们在计算 前缀和 sum[i] 的时候,其实也就已经得知 i 数组前面所有的下标的前缀和,那么我们可以利用哈希表来保存这些数据,哈希表存储的应该是 <sum[i], 出现的次数>

我们其实不需要创建前缀和数组
因为我们只是要利用前一个前缀和结果来推导现在这一个前缀和,对于前几次的前缀和结果我们其实用不到,所以我们可以使用一个变量 sum,一直滚动累加下去即可。

要事先将 <0,1> 存进哈希表中
因为可能会出现一开始就得到 和为 K 的数组,此时 sum - k = 0,但是 count 却没有自增,所以我们事先设定存在一个前缀和 为 0 的子数组,并且出现次数为 1

class Solution {    public int subarraySum(int[] nums, int k) {        Map<Integer,Integer> map = new HashMap<>();        map.put(0,1);        int sum = 0;        int count = 0;        for(int x : nums) {            sum += x;            if(map.containsKey(sum - k)) {                count += map.get(sum-k);            }            map.put(sum,map.getOrDefault(sum,0) + 1);        }        return count;    }}

和可被 K 整除的子数组

https://leetcode.cn/problems/subarray-sums-divisible-by-k/description/

在这里插入图片描述


解析:

这里依旧不能使用滑动窗口来做
因为存在负数,不具有单调性。

补充知识:同余定理,当 (a - b) % p = 0 可以推导出 a % p = b % p,有了这个公式,我们就可以得出当存在另外几个 b 满足 a % p = b % p 的时候,则说明存在 和可被 K 整除的子数组,在判断的时候,我们用到的是余数,所以在哈希表中,我们保存的应该为 <余数,余数出现的次数>,后面就是基本的前缀和操作了,和上面那一道题目类似。

Java 当中,负数 % 正数 的结果为 负数,我们需要将结果纠正为正数,公式为 ret = (sum % k + k) % k
解释一下,sum % k 可以得到余数,但是可能为负数,所以再加 一个 k,保证这是一个正数,但是可能会破坏余数这个特性,所以还要再次 模 k,最后的 模k 是不会影响的。

由于可能会出现一开始前缀和 正好为 K ,取模结果为0,此时应该预先将 <0,1> 存储进哈希表里。

class Solution {    public int subarraysDivByK(int[] nums, int k) {        Map<Integer,Integer> map = new HashMap<>();        map.put(0,1);        int sum = 0;        int count = 0;        for(int x : nums) {            sum += x;            int ret = (sum % k + k) % k;            count += map.getOrDefault(ret, 0);            map.put(ret,map.getOrDefault(ret,0) + 1);        }        return count;    }}

连续数组

https://leetcode.cn/problems/contiguous-array/description/

在这里插入图片描述

解析:

由于数组只会出现 0 或者 1,我们要寻找最大长度的数组(满足 0 的数量等于 1 的数量),如果要使用前缀和,我们可以将 0 设置为 -1,这样,我们要寻找的子数组(记为 K)的和就是为 0

前缀和数组sum[i] :

在这里插入图片描述

此时 K = 0,那么 就是要得到目前是否存在 sum[j] 数组是等于 sun[i],如果存在,则需要计算这个 K 数组的区间长度 = i - j

于是我们可以将前缀和 与 对应的下标索引 存到哈希表里,<sum,下标>

我们不用真的创建一个前缀和数组,因为我们用不到,我们可以使用一个变量 sum 来保存当前的前缀和结果

如果当整个数组的和为 K 的时候,那么最大的长度应该为 整个数组的长度,此时 i 最大到达 nums.length - 1 ,要想获得 数组长度,我们应该预先将 <0, -1> 存储进哈希表中

哈希表的 put :
我们保存数据的时候,要保存的是 sum 对应的最小的索引,因为我们要求的是最大的数组长度,所以当哈希表存在 sum[i] 的时候,我们应该更新最新长度,这个时候,就不用保存进哈希表里了,因为我们不需要跟新下标索引值,如果没有出现的话,那么此时 sum[i] 所对应的下标要一起保存进哈希表中。

class Solution {    public int findMaxLength(int[] nums) {        Map<Integer,Integer> map = new HashMap<>();        map.put(0,-1);        int n = nums.length;        int sum = 0;        int maxLength = 0;        for(int i = 0; i < n; i++) {            sum += (nums[i] == 0 ? -1 : 1);            if(map.containsKey(sum)) {                int j = map.get(sum);                maxLength = Math.max(maxLength,i - j);            } else {                map.put(sum,i);            }        }        return maxLength;    }}

矩阵区域和

https://leetcode.cn/problems/matrix-block-sum/description/

在这里插入图片描述

解析:

这道题目就是二维数组前缀和的使用

我们先创建好前缀和数组,然后锁定求解的区域,最后计算即可

这里要注意的是前缀和数组要和我们的最终数组要一一对应

class Solution {    public int[][] matrixBlockSum(int[][] mat, int k) {        int n = mat.length;        int m = mat[0].length;        int[][] dp = new int[n+1][m+1];                for(int i = 1; i <= n; i++) {            for(int j = 1; j <= m; j++) {                dp[i][j] = dp[i-1][j] + dp[i][j-1] - dp[i-1][j-1] + mat[i-1][j-1];            }        }        int[][] ans = new int[n][m];        for(int i = 0; i < n; i++) {            for(int j = 0; j < m; j++) {                int r1 = (i - k <= 0 ? 1 : i - k + 1);                int c1 = (j - k <= 0 ? 1 : j - k + 1);                int r2 = (i + k >= n ? n : i + k + 1);                int c2 = (j + k >= m ? m : j + k + 1);                ans[i][j] = dp[r2][c2] - (dp[r1-1][c2] + dp[r2][c1-1] - dp[r1-1][c1-1]);            }        }        return ans;    }}

点击全文阅读


本文链接:http://zhangshiyu.com/post/157781.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1