实现前缀和问题

303. 区域和检索 - 数组不可变

  1. class NumArray {
  2. private int[] sums;
  3. public NumArray(int[] nums) {
  4. sums = new int[nums.length + 1];
  5. for(int i = 1; i <= nums.length; ++i){
  6. sums[i] = nums[i-1] + sums[i-1];
  7. }
  8. }
  9. public int sumRange(int left, int right) {
  10. return sums[right + 1] - sums[left];
  11. }
  12. }

304. 二维区域和检索 - 矩阵不可变

class NumMatrix {

    private int[][] sums;

    public NumMatrix(int[][] matrix) {
        sums = new int[matrix.length + 1][matrix[0].length + 1];
        for(int i = 0; i < matrix.length; ++i){
            for(int j = 0; j < matrix[0].length; ++j){
                sums[i + 1][j + 1] = sums[i + 1][j] + sums[i][j + 1] - sums[i][j] + matrix[i][j];
            }
        }
    }

    public int sumRegion(int row1, int col1, int row2, int col2) {
        return    sums[row2 + 1][col2 + 1] 
                - sums[row1][col2 + 1] 
                - sums[row2 + 1][col1] 
                + sums[row1][col1];
    }
}

数据结构维护前缀和问题

几个经典问题

  • 前缀和与差分 - 图1上有没有一个区间,其和为target
    • HashSet
  • 前缀和与差分 - 图2上有多少个区间,其和为target
    • HashMap
  • 前缀和与差分 - 图3上有没有一个区间,其和大于/小于target
    • 线段树
  • 一棵树上有没有某个路径,其和为target

    HashMap I — {前缀和 : 第一次出现的下标}

    325. 和等于 k 的最长子数组长度

    要想得到和为k的子数组,问题可转化为「当前位置之前是否存在和为sums[i] - k的前缀和」
    值得学习的点

  • new HashMap<Integer, Integer>(){{put(0, -1);}};

  • map.putIfAbsent(sum, i);我们希望只有不存在这个键的时候才放入,有的话不修改原键值对

      public int maxSubArrayLen(int[] nums, int k){
          //{sum:index}
          HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
          map.put(0, -1);
          int ans = 0;
          int sum = 0;
          for (int i = 0; i < nums.length; ++i) {
              sum += nums[i];
              if (map.containsKey(sum - k)) {
                  ans = Math.max(ans, i - map.get(sum - k));
              }
              map.putIfAbsent(sum, i);
          }
          return ans;
      }
    

    525. 连续数组

    「相同数量的0和1的子数组」相当于「前缀和等于区间长度的一半」
    我的思路
    用一个hashmap保存前缀和出现的第一个索引,然后遍历到nums[i]时,查看map中有无前缀和等于长度的一半,即sum - key = (i - value) / 2,这样有个问题,就是连续的两个零对前缀和没影响,但是对长度有影响
    题解
    不妨将0转换成-1,那么当0和1数量相等时,区间的和就从「区间长度的一半」变成了「0」,这样就转换为上一题了

    1371. 每个元音包含偶数次的最长子字符串

    题解
    为每个元音维护一个前缀和数组,pre[i][k]表示前i个字符中,第k个元音出现的次数,则对于任意区间[l, r]是否满足条件,只需要对每个k计算pre[l+1][k] - pre[r][k]是否为偶数即可
    同样,应该考虑使用hashmap来存储前缀和,但是首先每个元音需要一个状态计算次数,其次,哈希表应该满足的关系为sum - key % 2 == 0,除了遍历所有的key,我们没法在O(1)内确定这个key,从而找到他的下标。考虑将「偶数次」这个条件还能代表什么,如果pre[l+1][k] - pre[r][k]为偶数,那么pre[l+1][k]pre[r][k]应该奇偶性相同。因此将前缀和从「元音出现的次数」修改为「元音出现次数的奇偶性」,如果当前前缀和是奇数,那么我们找到前缀和是奇数的最早出现的位置,即是符合条件的最大子串。
    同时,如果在上面的思路中,我们对字符串的某个前缀进行计算,产生的结果,即哈希表的键,应该是每个元音出现的奇偶性,有些冗余,且得需要一个新的数据结构,比如数组int[char - 'a'] = 0 or 1,而且如果题目扩大字符集,问非元音,则键会更冗余且不以维护。因为奇偶性无非就两个值,考虑使用一个k位二进制数,第k位表示第k个字符出现的奇偶性,例如u o i e a :1 1 0 1 1,因此可以讲hashmap优化成一个int数组,我们只需要查找数组中之前是否出现过该奇偶状态即可

      public int findTheLongestSubstring(String s) {
          int n = s.length();
          int[] pos = new int[1 << 5];
          Arrays.fill(pos, -1);
          int ans = 0, status = 0;
          pos[0] = 0;
          for (int i = 0; i < n; i++) {
              char ch = s.charAt(i);
              if (ch == 'a') {
                  status ^= (1 << 0);
              } else if (ch == 'e') {
                  status ^= (1 << 1);
              } else if (ch == 'i') {
                  status ^= (1 << 2);
              } else if (ch == 'o') {
                  status ^= (1 << 3);
              } else if (ch == 'u') {
                  status ^= (1 << 4);
              }
              if (pos[status] >= 0) {
                  ans = Math.max(ans, i + 1 - pos[status]);
              } else {
                  pos[status] = i + 1;
              }
          }
          return ans;
      }
    

    HashMap II—{前缀和 : 出现的次数}

    560. 和等于K 的子数组数量

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

    1248. 统计「优美子数组」

    「优美」 == 「恰好 k 个奇数」,k是指定的
    偶数累加一定是偶数,奇数累加取决于奇数的个数,这样只能判断有奇数个奇数还是偶数个奇数,这个思路不行。
    思路一:
    将偶数转换为0,奇数转换为1,出现k次奇数就变成了「和为k的子数组数量」,转换为上一题
    思路二
    将前缀的思想变成「前i个字符中奇数的个数」,则pre[i] = pre[i-1] + (nums[i]&1),因此区间的奇数恰好为k个可以转化为pre[i] - pre[j-1] == k,跟思路一类似,但是可以使用数组代替hashmap

    HashMap III—同时需要前缀和、后缀和

    238. 除自身以外数组的乘积

  • 构造一个前缀数组、一个后缀数组、一个答案数组

  • 构造一个答案数组,从前往后遍历第一遍将前缀积填入,从后往前遍历第二遍,不断更新答案数组,用一个额外的空间保存当前元素右边的后缀积

    724. 寻找数组的中心下标

    2 * pre[i] + nums[i] = total

    1477. 找两个和为目标值且不重叠的子数组

    二维前缀和

    自己编的. 元素和为目标值的最大子矩阵面积

    根据下一题,可能会出现的题目,因为 325. 和等于 k 的最长子数组长度
    560. 和等于K 的子数组数量,分别问了数组长度和数组个数。
    在下一题中,哈希表中保存的是前缀面积:出现的次数,则这个问题应该保存的是前缀面积:第一次出现的左边界,没有题目检验对不对,大体思路是这样的

      public int maxSubMatrixLen(int[][] matrix, int target) {
          int n = matrix.length, m = matrix[0].length;
          int[][] pre = new int[n + 1][m + 1];
          for (int i = 0; i < n; ++i) {
              for (int j = 0; j < m; ++j) {
                  pre[i + 1][j + 1] = pre[i][j + 1] + pre[i + 1][j] - pre[i][j] + matrix[i][j];
              }
          }
          int ans = 0;
          for (int top = 0; top < n; ++top) {
              for (int bot = top; bot < n; ++bot) {
                  int cur = 0;
                  Map<Integer, Integer> map = new HashMap<>();
                  map.put(0, -1);
                  for (int right = 0; right < m; ++right) {
                      cur = pre[bot + 1][right + 1] - pre[top][right + 1];
                      if (map.containsKey(cur - target)) {
                          ans = Math.max(ans, (bot - top + 1) * (right - map.get(cur - target)));
                      }
                      map.putIfAbsent(cur, right);
                  }
              }
          }
          return ans;
      }
    

    1074. 元素和为目标值的子矩阵数量

    前缀和暴力解
    注意里面的那个边界,即(p, q)是枚举左上角的位置,他是可以取到(i, j)的,跟(i, j)本身从 0 开始还是从 1 开始无关

      public int numSubmatrixSumTarget(int[][] matrix, int target) {
          int n = matrix.length, m = matrix[0].length;
          int[][] pre = new int[n + 1][m + 1];
          for (int i = 0; i < n; ++i) {
              for (int j = 0; j < m; ++j) {
                  pre[i + 1][j + 1] = pre[i][j + 1] + pre[i + 1][j] - pre[i][j] + matrix[i][j];
              }
          }
          int ans = 0;
          for (int i = 0; i < n; ++i) {
              for (int j = 0; j < m; ++j) {
                  for (int p = 0; p <= i; ++p) {
                      for (int q = 0; q <= j; ++q) {
                          int temp = pre[i + 1][j + 1] - pre[p][j + 1] - pre[i + 1][q] + pre[p][q];
                          if (temp == target){
                              ans++;
                          }
                      }
                  }
              }
          }
          return ans;
      }
    

    哈希表优化—抽象一维
    在一维数组中,我们枚举点,判断该点左边,是否存在某个前缀和等于pre - target,哈希表保存前缀和:第一次出现的下标/出现的次数。如何扩展到二维数组?在二维数组中,取一个子矩阵数组,如果上边界==下边界,那么他就是一个一维数组。我们考虑,枚举上下边界,确定一个大的子矩阵,然后枚举右边界,相当于一维中枚举点,然后判断该边的左边,是否存在某个前缀和等于pre - target,则找到了左边界,因此哈希表应该保存面积:第一次出现的坐标/出现的次数
    image.png

      public int numSubMatrixSumTarget(int[][] matrix, int target) {
          int n = matrix.length, m = matrix[0].length;
          int[][] pre = new int[n + 1][m + 1];
          for (int i = 0; i < n; ++i) {
              for (int j = 0; j < m; ++j) {
                  pre[i + 1][j + 1] = pre[i][j + 1] + pre[i + 1][j] - pre[i][j] + matrix[i][j];
              }
          }
          int ans = 0;
          for (int top = 0; top < n; ++top) {
              for (int bot = top; bot < n; ++bot) {
                  int cur = 0;
                  Map<Integer, Integer> map = new HashMap<>();
                  map.put(0, 1); //效果等同于注释部分
                  for (int right = 0; right < m; ++right) {
                      cur = pre[bot + 1][right + 1] - pre[top][right + 1];
    //                    if (cur == target) {
    //                        ans ++;
    //                    }
                      if (map.containsKey(cur - target)) {
                          ans += map.get(cur - target);
                      }
                      map.put(cur, map.getOrDefault(cur, 0) + 1);
                  }
              }
          }
          return ans;
      }
    

    363. 矩形区域不超过 K 的最大数值和

    该题无法由 325. 和等于 k 的最长子数组长度560. 和等于K 的子数组数量一维提升为二维,因为这是一个全新的问题,转换为一维应该描述为「和不超过K的子数组最大和」
    前缀和暴力解
    跟上题一样

      public int maxSumSubmatrix(int[][] mat, int k) {
          int m = mat.length, n = mat[0].length;
          int[][] sum = new int[m + 1][n + 1];
          for (int i = 1; i <= m; i++) {
              for (int j = 1; j <= n; j++) {
                  sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + mat[i - 1][j - 1];
              }
          }
          int ans = Integer.MIN_VALUE;
          for (int i = 1; i <= m; i++) {
              for (int j = 1; j <= n; j++) {
                  for (int p = i; p <= m; p++) {
                      for (int q = j; q <= n; q++) {
                          int cur = sum[p][q] - sum[i - 1][q] - sum[p][j - 1] + sum[i - 1][j - 1];
                          if (cur <= k) {
                              ans = Math.max(ans, cur);
                          } 
                      }
                  }
              }
          }
          return ans;
      }
    

    哈希表+二分—抽象一维

    class Solution {
      public int maxSumSubmatrix(int[][] mat, int k) {
          int m = mat.length, n = mat[0].length;
    
          // 预处理前缀和
          int[][] sum = new int[m + 1][n + 1];
          for (int i = 1; i <= m; i++) {
              for (int j = 1; j <= n; j++) {
                  sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + mat[i - 1][j - 1];
              }
          }
    
          int ans = Integer.MIN_VALUE;
          // 遍历子矩阵的上边界
          for (int top = 1; top <= m; top++) {
              // 遍历子矩阵的下边界
              for (int bot = top; bot <= m; bot++) {
                  // 使用「有序集合」维护所有遍历到的右边界
                  TreeSet<Integer> ts = new TreeSet<>();
                  ts.add(0);
                  // 遍历子矩阵的右边界
                  for (int r = 1; r <= n; r++) {
                      // 通过前缀和计算 right
                      int right = sum[bot][r] - sum[top - 1][r];
                      // 通过二分找 left
                      Integer left = ts.ceiling(right - k);
                      if (left != null) {
                          int cur = right - left;
                          ans = Math.max(ans, cur);
                      }
                      // 将遍历过的 right 加到有序集合
                      ts.add(right);
                  }
              }
          }
          return ans;
      }
    }
    

    1314. 矩阵区域和

    前缀和+控制坐标即可

    面试题 17.24. 最大子矩阵

    还是使用前缀和+抽象一维,在使用TreeMap在O(1)的时间复杂度内找到最小的左边界

    public int[] getMaxMatrix(int[][] matrix) {
      int n = matrix.length, m = matrix[0].length;
      int[] ans = new int[4];
      //pre sum
      int[][] pre = new int[n + 1][m + 1];
      for (int i = 0; i < n; ++i) {
          for (int j = 0; j < m; ++j) {
              pre[i + 1][j + 1] = pre[i + 1][j] + pre[i][j + 1] - pre[i][j] + matrix[i][j];
          }
      }
      int sum = Integer.MIN_VALUE / 2;
      for (int top = 0; top < n; ++top) {
          for (int bot = top; bot < n; ++bot) {
              TreeMap<Integer, Integer> tm = new TreeMap<>();
              tm.put(0, 0);
              for (int right = 0; right < m; ++right) {
                  int rightSum = pre[bot + 1][right + 1] - pre[top][right + 1];
                  Map.Entry<Integer, Integer> firstEntry = tm.firstEntry();
                  int leftSum = firstEntry.getKey();
                  if (rightSum - leftSum > sum) {
                      sum = rightSum - leftSum;
                      ans[0] = top;
                      ans[1] = firstEntry.getValue();
                      ans[2] = bot;
                      ans[3] = right;
                  }
                  tm.put(rightSum, right+1);
              }
          }
      }
      return ans;
    }
    

    运算推广

    前缀积

    「前缀积」==「取对数后前缀和」,有时为了避免超限制可以这么操作
    

    152. 乘积最大子数组

    动态规划
    注意正负号
    前缀积
    不太好做,没啥意义

    713. 乘积小于K的子数组

    前缀积
    超时且越限制,因为我把所有数的乘积都算出来了,实际上没必要,随时更新就好了,如果他大于k,跳出循环即可,是一种暴力方法

    public int numSubarrayProductLessThanK(int[] nums, int k) {
      int len = nums.length;
      int ans = 0;
      for(int i=0; i<len; i++) {
          int preMul = 1;
          for(int j=i; j<len; j++) {
              preMul = preMul * nums[j];
              if(preMul < k) {
                  ans++;
              } else {
                  break;
              }
          }
      }
      return ans;
    }
    

    前缀对数和
    将所有数取对数,然后将前缀积转换为前缀对数和,这样可以不超限制,这样我们可以用二分,在logn时间复杂度内找出符合条件的最大的前缀,然后这个前缀对应的下标之后的所有下标,都可以作为数组的左边界
    双指针
    上面的暴力还可以优化,无须遍历左边界,设置一个指针如果积大了,那就移动左指针,直到积小于k,然后答案加上左右指针的距离
    代码中条件**left <= right**是为了防止k为0的情况,可以在前面添加判断条件if k <= 1, return 0

    public int numSubarrayProductLessThanK(int[] nums, int k) {
      int n = nums.length;
      int ans = 0;
      int left = 0;
      int pre = 1;
      for (int right = 0; right < n; ++right) {
          pre *= nums[left];
          while (left <= right && pre >= k) {
              pre /= nums[left++];
          }
          ans += right - left + 1;
      }
      return ans;
    }
    

    1352. 最后 K 个数的乘积

    维护一个前缀积,遇到零就清空数组,最后求结果的时候如果数组长度小于k,说明里面有0,返回0即可
    其实还有进阶问题,如果是任意区间的乘积怎么算,可以考虑维护一个0的前缀和,如果区间右端点-左端点的前缀和无变化,那么说明没有0,不然就是有0

    前缀异或

    1310. 子数组异或查询

    pre[i] = arr[0]^...^arr[i-1]
    pre[j+1] = arr[0]^...^arr[i-1]^arr[i]^...^arr[j]
    ans[i:j] = pre[j+1] ^ pre[i]
    a^b = c, 那么a^b^b = a = c^b

    1442. 形成两个异或相等数组的三元组数目

    三重循环
    居然过了
    a == b <==> pre[j] ^ pre[i] == pre[k + 1] ^ pre[j]
    pre[i] == pre[k + 1]

      public int countTriplets(int[] arr) {
          int n = arr.length;
          if (n < 2) {
              return 0;
          }
          int[] pre = new int[n + 1];
          for (int i = 0; i < n; ++i) {
              pre[i + 1] = pre[i] ^ arr[i];
          }
          int ans = 0;
          for (int i = 0; i < n; ++i) {
              for (int j = i + 1; j < n; ++j) {
                  for (int k = j; k < n; ++k) {
                      int a = pre[j] ^ pre[i];
                      int b = pre[k + 1] ^ pre[j];
                      if (a == b) {
                          ans++;
                      }
                  }
              }
          }
          return ans;
      }
    

    二重循环
    由上面a == b <==> pre[i] == pre[k + 1],可知,只要两个前缀相等,j是无关紧要的,换句话说[i+1 : k]中的任何一个j都可以,也就是说有k-i个三元组,只需要枚举i、k即可

      public int countTriplets(int[] arr) {
          int n = arr.length;
          int[] s = new int[n + 1];
          for (int i = 0; i < n; ++i) {
              s[i + 1] = s[i] ^ arr[i];
          }
          int ans = 0;
          for (int i = 0; i < n; ++i) {
              for (int k = i + 1; k < n; ++k) {
                  if (s[i] == s[k + 1]) {
                      ans += k - i;
                  }
              }
          }
          return ans;
      }
    

    一重循环
    接着上面分析,对于下标k,若下标i = i1,i2,i3...,im时都满足 pre[i] == pre[k + 1],因此这些下标对答案的贡献为(k-i1)+(k-i2)+...+(k-im) = m*k - (i1 + i2 +...+im),也就是说,在遍历下标k的时候,pre[i] == pre[k + 1]时,我们需要知道「下标i出现的次数m」和「下标i之和」

      public int countTriplets(int[] arr) {
          int n = arr.length;
          // 预处理前缀异或数组
          int[] sum = new int[n + 1];
          for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] ^ arr[i - 1];
          int ans = 0;
          // 记录出现过的异或结果,存储格式:{ 异或结果 : [下标1, 下标2 ...] }
          Map<Integer, List<Integer>> map = new HashMap<>();
          for (int k = 0; k <= n; k++) {
              List<Integer> list = map.getOrDefault(sum[k], new ArrayList<>());
              for (int idx : list) {
                  int i = idx + 1;
                  ans += k - i;
              }
              list.add(k);
              map.put(sum[k], list);
          }
          return ans;
      }
    

    差分

    前缀和差分—>原序列
    image.png

    370. 区间加法

      public int[] getModifiedArray(int length, int[][] updates) {
          int[] ans = new int[length];
          int[] diff = new int[length];
          //构造差分
    //        diff[0] = ans[0] - 0;
    //        for (int i = 1; i < length; ++i) {
    //            diff[i] = ans[i] - ans[i - 1];
    //        }
          for (int[] update : updates) {
              diff[update[0]] += update[2];
              if (update[1] + 1 < length)
                  diff[update[1] + 1] -= update[2];
          }
          ans[0] = diff[0];
    //        for (int i = 1; i < length; ++i) {
    //            ans[i] = ans[i - 1] + diff[i];
    //        }
    //        return ans;
          //直接原地修改即可
          for (int i = 1; i < length; ++i) {
              diff[i] += diff[i - 1];
          }
          return diff;
      }