算法思想之前缀和(二)

欢迎拜访雾里看山-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;
    }
};

在这里插入图片描述

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

好的!下面是关于**朴素匹配算法**和**KMP(Knuth-Morris-Pratt)算法**的详细对比及介绍: --- ### **1. 朴素匹配算法 (暴力搜索)** #### 原理: 通过逐一检查主串 `S` 中每一个可能的位置,判断模式串 `P` 是否与该位置开始的一段字符完全一致。如果发现不匹配的情况,则移动到下一个起始位置继续尝试。 #### 步骤描述: 设字符串长度为主串 \( S \)长度为 \( n \),模式串 \( P \)长度为 \( m \)。 1. 初始化指针 i 和 j 分别指向主串的第一个字符以及模式串的第一个字符。 2. 如果当前字符相等(即 \( S[i] == P[j] \)),则同时递增两个指针。 3. 否则将主串回退至上次未检测过的起点之后一位再重新比较;此时需注意的是模式串也需要回到开头处再次进行配对流程。 4. 當 j 成功遍历整个模式串时意味着找到了一个完整的匹配项。 #### 时间复杂度分析: - 平均时间复杂度为 $O(n \times m)$,因为在最差的情况下每次失配都需要几乎完整地扫描一遍剩下的部分。 - 空间复杂度为常量级别,$O(1)$。 --- ### **2. KMP 匹配算法** #### 原理: 为了避免像朴素方法那样反复试探已知信息而导致冗余计算的问题,KMP 引入了一个额外的数据结构叫做“前缀表”或者说 “next 数组”,用于记录某些特定状态下的最长公共前后缀长度以便快速跳转过去从而提高效率。 #### 核心思想: 构建 next[] 表示每个位置对应的失效转移规则。一旦发生冲突可以直接利用之前累积的信息跳跃式前进而非单纯一步一步退回重试。 ##### 构造 Next 数组的过程: 以模式串 "ABABC"为例生成它的 next 数组过程如下所示: ``` Pattern : A B A B C Next : 0 0 1 2 0 ``` 解释上述结果: - 第位之前的元素都初始化为零; - 对于第三位 'A', 因其前面有单独字母'A’构成的相同子序列所以填入下一索引值也就是第一列; - 类似推理得到其他各项数值... #### 执行步骤: 有了这个辅助数组后就可以按照如下逻辑来进行文本查找了: 1. 设置双指针 p,q 分别代表 pattern 序列当前位置与 text 文本中的候选区域首地址。 2. 比较两者相应内容是否吻合;如果不符就参照预先准备好的 next[p-1] 来调整pattern 移动步长而不是盲目向右挪移单格单位。 3. 当 q 能够顺利走过全程的时候便宣告成功定位到了一处有效嵌套区间内。 #### 时间复杂度分析: - 主循环只经过一次线性扫视便可结束任务故整体时间为 $ O(n+m) $ - 空间消耗同样维持在线形范围内即 $ O(m) $ --- ### **综合比较** | 方面 | Naive Algorithm | KMP Algorithm | |----------------|-------------------------|--------------------| | 实现难度 | 较低 | 高 | | 最优性能情况 | 较少 | 更加普遍 | | 数据预处理需求 | 无 | 存在 | | 单次失败后的代价 | 显著增加 | 几乎不变 | | 推荐使用场合 | 小型数据集简短查询 | 大规模批量检索场景 | 尽管Naive algorithm易于理解和编码实施,KMP凭借更高的运行效能成为更受青睐的选择尤其是在涉及大量实时反馈请求的服务端架构当中. ---
评论 34
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雾里看山

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值