Bootstrap

【Java算法】前缀和 下

  🔥个人主页: 中草药

🔥专栏:【算法工作坊】算法实战揭秘


一.连续数组

题目链接:525.连续数组

代码

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

算法原理

这段代码实现了一个寻找数组中具有相等数量的0和1的最长子数组长度的算法。具体来说,它使用了前缀和(prefix sum)的概念以及哈希映射(HashMap)来优化查找过程。

  1. 初始化哈希映射

    • 创建一个HashMap,用来存储前缀和与对应的数组索引。
    • 初始时,将前缀和为0的情况标记在索引-1处,这是为了处理从数组开始位置就有相等数量的0和1的情况。
  2. 计算前缀和

    • 遍历数组中的每一个元素nums[i]
    • 如果nums[i]是0,则将其视为-1,如果是1,则保持为1。这样,我们可以用加法来累积0和1的差值。
    • sum变量用于跟踪当前遍历到的位置为止的累积差值。
  3. 检查相等数量的0和1

    • 每次更新sum后,我们检查sum是否已经存在于HashMap中。
    • 如果存在,这意味着从前一次出现相同sum的点到当前点之间,0和1的数量是相等的(因为累积的差值没有改变)。
    • 我们可以通过当前索引i减去先前sum出现的索引来计算这个子数组的长度,并更新最长子数组的记录。
  4. 更新哈希映射

    • 如果sum不在HashMap中,说明我们首次遇到这个累积差值。我们将sum及其对应的索引i添加到HashMap中。
  5. 返回结果

    • 在遍历完整个数组后,返回最长子数组的长度。

这种方法的时间复杂度为O(n),因为我们只遍历数组一次,并且对每个元素执行常数时间的操作。空间复杂度也是O(n),最坏情况下需要存储n个前缀和值。

举例 

测试用例  nums = [0,1,0]
  1. 初始化阶段:

    • 哈希映射map被创建并初始化,其中map.put(0, -1),表示前缀和为0的初始位置在-1,这是为了处理从数组开始就有相等数量的0和1的情况。
  2. 开始遍历数组:

    • 对于nums[0]0,由于nums[i] == 0,因此sum增加-1,此时sum = -1
      • map中不存在键-1,所以将sum = -1和其对应的索引i = 0放入map中。
    • map现在包含{0: -1, -1: 0}
  3. 继续遍历:

    • 对于nums[1]1sum增加1(因为nums[i] == 1),此时sum = 0
      • map中存在键0,这表明从map.get(0) = -1(即数组的起始点之前)到当前索引i = 1之间的子数组[0, 1]有相等数量的0和1。
      • 计算子数组长度i - map.get(sum) = 1 - (-1) = 2,更新ret为最大值,即ret = 2
    • 此时map包含{0: -1, -1: 0, 0: 1},但是我们不需要保留旧的0键值对,通常这个实现中不会更新键值对,而是会在下一次遇到相同的sum时计算新的子数组长度。
  4. 最后一个元素:

    • 对于nums[2]0,再次sum增加-1,此时sum = -1
      • map中存在键-1,这表明从map.get(-1) = 0到当前索引i = 2之间的子数组[1, 0]有相等数量的0和1。
      • 计算子数组长度i - map.get(sum) = 2 - 0 = 2,但ret已经是2,所以ret保持不变。
  5. 返回结果:

    • 遍历结束后,ret的值为2,表示最长子数组的长度是2。

因此,对于nums = [0, 1, 0],这段代码会正确地返回最长子数组长度为2,即子数组[0, 1][1, 0]。值得注意的是,尽管最后的结果是正确的,但根据题目要求和具体实现细节,可能有多个满足条件的子数组,而这段代码仅返回长度信息,不提供具体的子数组元素。

二.和为k的子数组

题目链接:560.和为k的子数组

代码

public int subarraySum(int[] nums, int k) {
        HashMap<Integer,Integer> map=new HashMap<>();
        map.put(0,1);
        int ret=0,sum=0;
        for(int i=0;i<nums.length;i++){
            sum+=nums[i];
            ret+=map.getOrDefault(sum-k,0);
            map.put(sum,map.getOrDefault(sum,0)+1);
        }
        return ret;
    }

算法原理

这段代码实现了一个算法,用于找到数组nums中所有和等于给定值k的连续子数组的个数。该算法的核心思想同样基于前缀和和哈希映射,但这里的目标是在遍历过程中查找是否有任何前缀和能够与当前前缀和形成差值为k的配对。

以下是算法的详细步骤:

  1. 初始化哈希映射

    • 创建一个HashMap,用来存储前缀和与对应的出现次数。
    • 初始时,将前缀和为0的情况标记出现次数为1。这是为了处理从数组开始位置就有和等于k的子数组的情况。
  2. 初始化变量

    • ret用于累计满足条件的子数组的个数。
    • sum用于跟踪当前遍历到的位置为止的累积和。
  3. 遍历数组

    • 遍历数组中的每一个元素nums[i]
    • 更新sum,使其反映当前位置为止的累积和。
  4. 查找匹配的前缀和

    • 使用getOrDefault方法查询map中是否存在sum - k的前缀和。
    • 如果存在,意味着从某个位置到当前位置的累积和正好为kgetOrDefault返回的值是这个前缀和出现的次数,将其累加到ret中,代表找到了这么多满足条件的子数组。
  5. 更新哈希映射

    • 将当前的sum及其对应的出现次数更新到map中。如果sum已经在map中,那么它的计数器加1;否则,设置为1。
  6. 返回结果

    • 在遍历完整个数组后,ret包含了所有满足条件的子数组的个数,直接返回即可。

这种方法的时间复杂度同样是O(n),因为我们只遍历数组一次,并且每次操作都是常数时间的。空间复杂度为O(n),因为在最坏的情况下,哈希映射可能需要存储n个不同的前缀和。这种算法利用了哈希映射快速查找和插入的特点,从而实现了高效的解决方案。

举例

测试用例 nums = [1,2,3], k = 3
  1. 初始化阶段:

    • 创建哈希映射map并初始化,其中map.put(0, 1),表示前缀和为0的出现次数为1。
  2. 遍历数组:

    • 对于nums[0]1

      • 当前累积和sum更新为1
      • 查询map中是否存在sum - k = 1 - 3 = -2,显然不存在,因此ret不改变。
      • 更新mapmap.put(sum, map.getOrDefault(sum, 0) + 1),即map.put(1, 1),因为1的前缀和第一次出现。
    • 对于nums[1]2

      • 当前累积和sum更新为3
      • 查询map中是否存在sum - k = 3 - 3 = 0,存在并且出现次数为1,因此ret增加1,现在ret = 1
      • 更新mapmap.put(3, map.getOrDefault(3, 0) + 1),即map.put(3, 1),因为3的前缀和第一次出现。
    • 对于nums[2]3

      • 当前累积和sum更新为6
      • 查询map中是否存在sum - k = 6 - 3 = 3,存在并且出现次数为1,因此ret再增加1,现在ret = 2
      • 更新mapmap.put(6, map.getOrDefault(6, 0) + 1),即map.put(6, 1),因为6的前缀和第一次出现。
  3. 返回结果:

    • 遍历结束后,ret的值为2,表示有2个连续子数组的和等于k

通过上面的分析,我们发现确实有两个子数组的和等于k = 3,分别是[1, 2][3]。因此,这段代码对于测试用例nums = [1, 2, 3]k = 3,会正确返回2作为结果。

三.和可被k整除的子数组

题目链接:974.和可被k整除的子数组

代码

public int subarraysDivByK(int[] nums, int k) {
       HashMap<Integer,Integer> map=new HashMap<>();
        map.put(0,1);
        int ret=0,sum=0;
        for(int i=0;i<nums.length;i++){
            sum+=nums[i];
            int r=(sum%k+k)%k;//对负数的处理
            //根据,同余定理,(sum-x)%k==0,sum%k==0,x%k==0
            ret+=map.getOrDefault(r,0);
            map.put(r,map.getOrDefault(r,0)+1);
        }
        return ret;
    }

算法原理

        实现了一个算法,用于寻找数组nums中所有和能被整数k整除的连续子数组的个数。这里的算法原理与之前的略有不同,主要是针对模运算的特性进行优化,特别是处理负数模运算的情况。

        算法的关键点在于使用模运算的性质来简化问题,并利用哈希映射来存储和查找特定的模结果。以下是算法的详细步骤:

  1. 初始化哈希映射

    • 创建一个HashMap,用来存储模结果与对应的出现次数。
    • 初始时,将模结果为0的出现次数标记为1。这是因为如果累积和本身就能被k整除,那么从数组的起始位置到当前位置的子数组就满足条件。
  2. 遍历数组

    • 遍历数组中的每一个元素nums[i]
    • 更新sum,使其反映当前位置为止的累积和。
  3. 处理模运算

    • 为了确保模运算结果始终为正,使用(sum % k + k) % k的技巧。这是因为sum % k可能得到一个负数(当sum % k < 0时),而我们需要确保r始终在0k-1之间。这个技巧保证了即使sum % k是负数,r也能正确地映射到0k-1的范围内。
  4. 查找匹配的模结果

    • 使用getOrDefault方法查询map中是否存在r的模结果。
    • 如果存在,意味着从某个位置到当前位置的累积和模k的结果为r,且已有其他子数组也得到同样的模结果。getOrDefault返回的值是这个模结果出现的次数,将其累加到ret中,代表找到了这么多满足条件的子数组。
  5. 更新哈希映射

    • 将当前的模结果r及其对应的出现次数更新到map中。如果r已经在map中,那么它的计数器加1;否则,设置为1。
  6. 返回结果

    • 在遍历完整个数组后,ret包含了所有满足条件的子数组的个数,直接返回即可。

这种方法的时间复杂度仍然是O(n),因为我们只遍历数组一次,并且每次操作都是常数时间的。空间复杂度为O(k),因为在最坏的情况下,哈希映射可能需要存储k个不同的模结果。通过这种方式,算法有效地利用了模运算的性质和哈希映射的快速查找能力,实现了高效解决方案。

举例

测试用例 nums = [4,5,0,-2,-3,1], k = 5
  1. 初始化阶段:

    • 创建哈希映射map并初始化,其中map.put(0, 1),表示模结果为0的出现次数为1。
  2. 遍历数组:

    • 对于nums[0]4

      • 当前累积和sum更新为4
      • 计算模结果r = (sum % k + k) % k = (4 % 5 + 5) % 5 = 4
      • 查询map中是否存在r = 4,显然不存在,因此ret不改变。
      • 更新mapmap.put(4, map.getOrDefault(4, 0) + 1),即map.put(4, 1),因为4的模结果第一次出现。
    • 对于nums[1]5

      • 当前累积和sum更新为9
      • 计算模结果r = (9 % 5 + 5) % 5 = 4
      • 查询map中是否存在r = 4,存在并且出现次数为1,因此ret增加1,现在ret = 1
      • 更新mapmap.put(4, map.getOrDefault(4, 0) + 1),即map.put(4, 2),因为4的模结果第二次出现。
    • 对于nums[2]0

      • 当前累积和sum更新为9
      • 计算模结果r = (9 % 5 + 5) % 5 = 4
      • 查询map中是否存在r = 4,存在并且出现次数为2,因此ret再增加2,现在ret = 3
      • 更新mapmap.put(4, map.getOrDefault(4, 0) + 1),即map.put(4, 3),因为4的模结果第三次出现。
    • 对于nums[3]-2

      • 当前累积和sum更新为7
      • 计算模结果r = (7 % 5 + 5) % 5 = 2
      • 查询map中是否存在r = 2,显然不存在,因此ret不改变。
      • 更新mapmap.put(2, map.getOrDefault(2, 0) + 1),即map.put(2, 1),因为2的模结果第一次出现。
    • 对于nums[4]-3

      • 当前累积和sum更新为4
      • 计算模结果r = (4 % 5 + 5) % 5 = 4
      • 查询map中是否存在r = 4,存在并且出现次数为3,因此ret再增加3,现在ret = 6
      • 更新mapmap.put(4, map.getOrDefault(4, 0) + 1),即map.put(4, 4),因为4的模结果第四次出现。
    • 对于nums[5]1

      • 当前累积和sum更新为5
      • 计算模结果r = (5 % 5 + 5) % 5 = 0
      • 查询map中是否存在r = 0,存在并且出现次数为1,因此ret再增加1,现在ret = 7
      • 更新mapmap.put(0, map.getOrDefault(0, 0) + 1),即map.put(0, 2),因为0的模结果第二次出现。
  3. 返回结果:

    • 遍历结束后,ret的值为7,表示有7个连续子数组的和能被k = 5整除。

通过上面的分析,我们可以看到,确实有7个子数组的和能被5整除,例如[4][4, 5][4, 5, 0][5][5, 0][5, 0, -2, -3]、和[1]。因此,这段代码对于测试用例nums = [4, 5, 0, -2, -3, 1]k = 5,会正确返回7作为结果。

四.矩阵区域和

题目链接:1314.矩阵区域和

代码

 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<dp.length;i++){
            for(int j=1;j<dp[0].length;j++){
                dp[i][j]=dp[i-1][j]+dp[i][j-1]+mat[i-1][j-1]-dp[i-1][j-1];
                //由于边界情况的处理,故加上的mat[i][j]变为mat[i-1][j-1]
            }
        }
        //求解
        int[][] ret=new int[m][n];
        for(int i=0;i<ret.length;i++){
            for(int j=0;j<ret[0].length;j++){
                int x1=Math.max(0,i-k)+1;
                int y1=Math.max(0,j-k)+1;
                int x2=Math.min(m-1,i+k)+1;
                int y2=Math.min(n-1,j+k)+1;
                ret[i][j]=dp[x2][y2]-dp[x1-1][y2]-dp[x2][y1-1]+dp[x1-1][y1-1];
            }
        }
        return ret;
    }

算法原理

这段代码实现了一个算法,用于计算二维矩阵mat中每个元素为中心、边长为2*k+1的正方形区域内的元素之和,并将这些和组成一个新的矩阵ret返回。这里采用的是前缀和矩阵的方法,它是一种高效的空间换时间的策略,特别适用于解决二维范围查询问题。

算法的原理可以分为以下几步:

  1. 构建前缀和矩阵

    • 首先创建一个比原矩阵mat大一圈的二维数组dp,用于存储前缀和。额外的一圈是为了简化边界条件的处理。
    • 通过两重循环遍历dp矩阵的内部(从dp[1][1]开始),计算前缀和。
    • 前缀和的计算公式为:dp[i][j] = dp[i-1][j] + dp[i][j-1] + mat[i-1][j-1] - dp[i-1][j-1]
    • 这个公式实际上计算了以(i-1, j-1)为右下角的矩形区域内的元素之和。注意,由于dp矩阵的大小比mat大一圈,所以在公式中使用mat[i-1][j-1]
  2. 计算查询结果矩阵

    • 创建一个与mat大小相同的矩阵ret,用于存储最终结果。
    • 再次通过两重循环遍历mat矩阵,对于每个位置(i, j)
      • 确定查询的四个边界:左上角(x1, y1)和右下角(x2, y2),考虑到边界条件,确保它们都在矩阵的有效范围内。
      • 使用前缀和矩阵dp计算以(i, j)为中心的2*k+1大小的正方形区域内元素之和。公式为:ret[i][j] = dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1]
      • 这个公式实际上是利用前缀和矩阵的性质,从大矩形中减去三个不需要的小矩形区域,得到目标区域的元素之和。
  3. 返回结果矩阵

    • 完成所有位置的计算后,返回结果矩阵ret

这种方法的时间复杂度主要由两次遍历构成,分别是构建前缀和矩阵和计算查询结果,均为O(mn),其中m和n分别是矩阵的行数和列数。空间复杂度为O(mn),用于存储前缀和矩阵和结果矩阵。通过前缀和矩阵的使用,避免了对每个查询重复计算区域和,从而大大提高了效率。


🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀

以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐

  制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸

;