算法思想之前缀和(二)

发布于:2025-04-16 ⋅ 阅读:(18) ⋅ 点赞:(0)

欢迎拜访雾里看山-CSDN博客
本篇主题:算法思想之前缀和(二)
发布时间:2025.4.11
隶属专栏算法

在这里插入图片描述

算法介绍

核心思想

前缀和(Prefix Sum) 是一种预处理数组的方法,通过预先计算并存储数组的累积和,将区间和查询的时间复杂度从 O(n) 优化至 O(1),适用于频繁查询子数组和的场景。

大致步骤

  1. 预处理出来一个前缀和数组
  2. 使用前缀和数组
  3. 处理边界情况

例题

和为 K 的子数组

题目链接

560. 和为 K 的子数组

题目描述

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。

子数组是数组中元素的连续非空序列。

示例 1

输入:nums = [1,1,1], k = 2
输出:2

示例 2

输入:nums = [1,2,3], k = 3
输出:2

提示:

  • 1 <= nums.length <= 2 * 104
  • -1000 <= nums[i] <= 1000
  • -107 <= k <= 107

算法思路

i 为数组中的任意位置,用 sum[i] 表示 [0, i] 区间内所有元素的和。
想知道有多少个i 为结尾的和为 k 的子数组,就要找到有多少个起始位置为 x1, x2, x3... 使得 [x, i] 区间内的所有元素的和为 k 。那么 [0, x] 区间内的和是不是就是sum[i] - k 了。于是问题就变成:

  • 找到在 [0, i - 1] 区间内,有多少前缀和等于 sum[i] - k 的即可。

我们不用真的初始化一个前缀和数组,因为我们只关心在 i 位置之前,有多少个前缀和等于sum[i] - k 。因此,我们仅需用一个哈希表,一边求当前位置的前缀和,一边存下之前每一种前缀和出现的次数。

代码实现

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int, int> hash;
        hash[0] = 1;
        int n = nums.size(), sum = 0, ret = 0;
        for(int i = 0; i < n; i++)
        {
            sum += nums[i];
            if(hash[sum-k] > 0)
                ret += hash[sum-k];
            hash[sum]++;
        }
        return ret;
    }
};

在这里插入图片描述

和可被 K 整除的子数组

题目链接

974. 和可被 K 整除的子数组

题目描述

给定一个整数数组 nums 和一个整数 k ,返回其中元素之和可被 k 整除的非空 子数组 的数目。

子数组 是数组中 连续 的部分。

示例 1

输入:nums = [4,5,0,-2,-3,1], k = 5
输出:7
解释
有 7 个子数组满足其元素之和可被 k = 5 整除:
[4, 5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3]

示例 2:

输入: nums = [5], k = 9
输出: 0

提示:

  • 1 <= nums.length <= 3 * 104
  • -104 <= nums[i] <= 104
  • 2 <= k <= 104

算法思路

补充两个小知识

  • 同余定理
    如果 (a - b) % n == 0,那么我们可以得到一个结论: a % n == b % n 。用文字叙述就是,如果两个数相减的差能被 n 整除,那么这两个数对 n 取模的结果相同。
    例如:(26 - 2) % 12 == 0 ,那么 26 % 12 == 2 % 12 == 2
  • c++ 中负数取模的结果,以及如何修正负数取模的结果
    • c++ 中关于负数的取模运算,结果是把负数当成正数,取模之后的结果加上一个负号
      例如: -1 % 3 = -(1 % 3) = -1
    • 因为有负数,为了防止发生出现负数的结果,以 (a % n + n) % n 的形式输出保证为正。
      例如: -1 % 3 = (-1 % 3 + 3) % 3 = 2

设 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(vector<int>& nums, int k) {
        unordered_map<int, int> hash;
        hash[0%k] = 1;
        int sum = 0, ret = 0;
        for(auto &n : nums)
        {
            sum += n;
            int r = (sum%k + k)%k;
            if(hash[r] > 0)
                ret+=hash[r];
            hash[r]++;
        } 
        return ret;
    }
};

在这里插入图片描述

连续数组

题目链接

525. 连续数组

题目描述

给定一个二进制数组 nums , 找到含有相同数量的 01 的最长连续子数组,并返回该子数组的长度。

示例 1
输入:nums = [0,1]
输出:2
说明:[0, 1] 是具有相同数量 0 和 1 的最长连续子数组。

示例 2

输入:nums = [0,1,0]
输出:2
说明:[0, 1] (或 [1, 0]) 是具有相同数量 0 和 1 的最长连续子数组。

示例 3:

输入:nums = [0,1,1,1,1,1,0,0,0]
输出:6
解释:[1,1,1,0,0,0] 是具有相同数量 0 和 1 的最长连续子数组。
提示:

  • 1 <= nums.length <= 105
  • nums[i] 不是 0 就是 1

算法思路

稍微转化一下题目,就会变成我们熟悉的题:

  • 本题让我们找出一段连续的区间, 01 出现的次数相同。
  • 如果将 0 记为 -11 记为 1 ,问题就变成了找出一段区间,这段区间的和等于 0
    • 于是,就和 560. 和为 K 的子数组 这道题的思路一样

代码实现

class Solution {
public:
    int findMaxLength(vector<int>& nums) {
        unordered_map<int, int> hash;
        hash[0] = -1;
        int n = nums.size(), sum = 0, ret = 0;
        for(int i = 0; i < n; i++)
        {
            sum += (nums[i] == 0 ? -1: 1);
            if(hash.count(sum))
                ret = max(ret, i - hash[sum]);
            else    
                hash[sum] = i;
        }
        return ret;
    }
};

在这里插入图片描述

矩阵区域和

题目链接

1314. 矩阵区域和

题目描述

给你一个 m x n 的矩阵 mat 和一个整数 k ,请你返回一个矩阵 answer ,其中每个 answer[i][j] 是所有满足下述条件的元素 mat[r][c] 的和:

  • i - k <= r <= i + k,
  • j - k <= c <= j + k
  • (r, c) 在矩阵内。

示例 1

输入:mat = [[1,2,3],[4,5,6],[7,8,9]], k = 1
输出:[[12,21,16],[27,45,33],[24,39,28]]

示例 2

输入:mat = [[1,2,3],[4,5,6],[7,8,9]], k = 2
输出:[[45,45,45],[45,45,45],[45,45,45]]

提示

  • m == mat.length
  • n == mat[i].length
  • 1 <= m, n, k <= 100
  • 1 <= mat[i][j] <= 100

算法思路

⼆维前缀和的简单应用题,关键就是我们在填写结果矩阵的时候,要找到原矩阵对应区域的左上角以及右下角的坐标(推荐画图)

  • 左上角坐标: x1 = i - ky1 = j - k ,但是由于会超过矩阵的范围,因此需要对 0取一个 max 。因此修正后的坐标为: x1 = max(0, i - k), y1 = max(0, j - k) ;
  • 右下角坐标: x1 = i + ky1 = j + k ,但是由于会超过矩阵的范围,因此需要对 row - 1 ,以及 col - 1 取⼀个 min 。因此修正后的坐标为: x2 = min(row - 1, i + k), y2 = min(col - 1, j + k)

然后将求出来的坐标代入到二维前缀和矩阵的计算公式上即可~(但是要注意下标的映射关系)

代码实现

class Solution {
public:
    vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
        int row = mat.size(), col = mat[0].size();
        vector<vector<int>> dp(row+1, vector<int>(col+1));
        for(int i = 1; i <= row; i++)
            for(int j = 1; j <= col; j++)
                dp[i][j] = dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+mat[i-1][j-1];
        vector<vector<int>> answer(row, vector<int>(col));
        for(int i = 0; i < row; i++)
            for(int j = 0; j < col; j++)
            {
                int x1 = max(0, i-k)+1, y1 = max(0, j-k)+1;
                int x2 = min(row-1, i+k)+1, y2 = min(col-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;
    }
};

在这里插入图片描述

⚠️ 写在最后:以上内容是我在学习以后得一些总结和概括,如有错误或者需要补充的地方欢迎各位大佬评论或者私信我交流!!!