【优选算法】Prefix-Kage:前缀和的算法影(下)

发布于:2024-12-22 ⋅ 阅读:(41) ⋅ 点赞:(0)

文章目录

  • 1.前缀和+后缀和
    • 1.1 寻找数组的中心下标
    • 1.2 除自身以外数组的乘积
  • 2.前缀和+哈希表
    • 2.1 和为k的子数组
    • 2.2 和可被k整除的子数组
    • 2.3 连续数组
  • 3.二维前缀和拓展
    • 3.1 矩阵区域和
  • 希望读者们多多三连支持
  • 小编会继续更新
  • 你们的鼓励就是我前进的动力!

本篇是前缀和算法的实战篇,有一定难度,相信耐心看完会有别样的收获:)

1.前缀和+后缀和

1.1 寻找数组的中心下标

✏️题目描述:

在这里插入图片描述

✏️示例:

在这里插入图片描述

传送门:寻找数组的中心下标

题解:
💻第一步: 要计算前后和相等的数的下标(不包括该数),显然要计算前后缀和

在这里插入图片描述

假设符合题意的数的下标为 i,那么其前后数的下标就很容易推导出来了。我们小学的时候就学过函数,那么前后和是不是也可以用一个函数表示,只要有符合的 i 直接代入即可

f:前缀和数组 → f[i] 表示:[0,i-1] 区间所有元素的和
g:后缀和数组 → g[i] 表示:[i+1,n-1] 区间所有元素的和

所以根据一维前缀和模版的公式可得 f[i] = f[i - 1] + nums[i - 1]g[i] = g[i + 1] + nums[i + 1],注意这个后缀和是从后往前加推导的

💻第二步:

在这里插入图片描述

接着遍历题目的数组,枚举出符合 f[i] == g[i] 情况的下标就行了

💻细节问题:

根据我们写的两个公式,当 i = 0i = n - 1 时,即第一个数和最后一个数的下标,发现会出现 f[-1]g[n] 的情况,也就是数组越界,所以前缀和从第二个数开始后缀和从倒数第二个数开始,那么第一个数和最后一个数会不会访问不到?答案是不会的后缀和会访问到第一个数前缀和会访问到最后一个数。因此当 i最左侧最右侧时,令 f[0]0g[n-1]0 来处理这种特殊情况

💻代码实现:

#include <vector>
#include <iostream>
using namespace std;

class Solution 
{
public:
    int pivotIndex(vector<int>& nums) 
    {
        int n = nums.size();
        vector<int> f(n), g(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;
    }
};

如果数组存在中心下标,返回 1不存在中心下标,返回 -1

1.2 除自身以外数组的乘积

✏️题目描述:

在这里插入图片描述

✏️示例:

在这里插入图片描述

传送门:除自身以外数组的乘积

题解:
💻细节问题:
本题和寻找数组的中心下标思路基本一致,因为要相乘,所以令 f[0] = 1g[n-1] = 1,要额外创建一个数组存放各个位置下乘积的结果

💻代码实现:

#include <vector>
#include <iostream>
using namespace std;

class Solution 
{
public:
    vector<int> productExceptSelf(vector<int>& nums)
    {
        //1.读取数据
        int n = nums.size();
        vector<int> f(n), g(n);
        //2.预处理前缀积和后缀积
        f[0] = 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];
        }
        //3.使用
        vector<int> ret(n);
        for (int i = 0; i < n; ++i)
        {
            ret[i] = f[i] * g[i];
        }
        return ret;
    }
};

2.前缀和+哈希表

2.1 和为k的子数组

✏️题目描述:

在这里插入图片描述

✏️示例:

在这里插入图片描述

传送门:和为k的子数组

题解:
💻第一步:

根据题意要求和为k的子数组,那么假设在某个下标为 i 处有符合题意的子数组前缀和sum 表示

在这里插入图片描述
那么就有 sum[i] - sum[j] = k移项可得 sum[i] - k = sum[j] ,所以要求和为 k 的子数组等同于在不断往前求前缀和 sum[i] 的过程中,计算 sum[i] - k 能否找到符合题意的 sum[j]

💻第二步:

在这里插入图片描述

因为前缀和可能存在重复的情况,我们又需要计算前缀和其对饮出现的次数,进而我们可以想到哈希表来处理这种情况

💻细节问题:

🚩前缀和加入哈希表的时机?

在不断往前求前缀和 sum[i] 的过程中,i 是不断变化的,我们是要找在 [0,i-1] 区间内,有多少个前缀和等于 sum[i] - k ,我们关心的是 sum[i]-k 是否在之前出现过以及出现的次数,这样就能知道有多少个子数组的和为 k

• 如果一次性把每个位置的前缀和都放入哈希表,会导致一些问题。例如,当有一个数组nums = [1,2,3],k = 3

• 假设我们在遍历过程中,不加判断地每次都放入哈希表。当第一次计算到前缀和为3(1 + 2)时放入哈希表,然后继续遍历到3这个元素时,前缀和变成了6,如果此时又放入哈希表,就会覆盖之前前缀和为3的记录

• 但是,从1开始到2的子数组和为3,从3本身这个元素也构成和为3的子数组,这两个情况是不同的,如果错误地覆盖了之前的记录,就无法正确统计出和为k的子数组数量

🚩不用真的创建一个前缀和数组

用一个变量sum来标记前一个位置的前缀和即可

🚩如果整个前缀和等于k呢?

那么就每次遍历开始前特殊的令 hash[0] = 1

💻代码实现:

#include <vector>
#include <iostream>
#include <unordered_map>
using namespace std;

class Solution 
{
public:
    int subarraySum(vector<int>& nums, int k) 
    {
        unordered_map<int, int> hash;
        hash[0] = 1;

        int sum = 0, ret = 0;
        for (auto x : nums)
        {
            sum += x;
            if (hash.count(sum - k))
            {
                ret += hash[sum - k];
            }
            hash[sum]++;
        }
        return ret;
    }
};

2.2 和可被k整除的子数组

✏️题目描述:

在这里插入图片描述

✏️示例:

在这里插入图片描述

传送门:和可被k整除的子数组

题解:
💻细节问题:

本题基本上与和为k的子数组思路一致

补充两个知识点:

  1. 同余定理
    在这里插入图片描述
  2. 负数结果修正成正数

在这里插入图片描述

因为数组里可能会存在负数,而数组下标没有负数,所以需要修正


在这里插入图片描述

所以根据同余定理,这里本质上就是在求在[0,i-1]区间内,有多少个前缀和的余数等于 sum % k

💻代码实现:

#include <vector>
#include <iostream>
#include <unordered_map>
using namespace std;

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 x : nums)
        {
            sum += x;
            int r = (sum % k + k) % k;
            if (hash.count(r))
            {
                ret += hash[r];
            }
            hash[r]++;
        }
        return ret;
    }
};

2.3 连续数组

✏️题目描述:

在这里插入图片描述

✏️示例:

在这里插入图片描述

传送门:连续数组

题解:
💻细节问题:

该题的思路也是前缀和+哈希表,要求和为有相同数量的 0 和 1,不妨令所有的 0 为 -1,也就把题目转化为 sum[i] - sum[j] = 0,即 sum[i] = sum[j],求在遍历前缀和的过程中能不能找到前面的一个前缀和与其相等,注意 hash[0] = -1

在这里插入图片描述

如果在后面又求到一个符合sum[i] = sum[j]的怎么办?

本题求的是最长连续子数组,所以两个是同种情况,无需考虑覆不覆盖的问题,无论哪个都可以

💻代码实现:

#include <vector>
#include <iostream>
#include <unordered_map>
using namespace std;

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

3.二维前缀和拓展

3.1 矩阵区域和

✏️题目描述:

在这里插入图片描述

✏️示例:

在这里插入图片描述

传送门:矩阵区域和

💻解读题意:
在这里插入图片描述
这题单看题目或许有点难理解,以示例 1 为例,每个方格里的数对应的求和矩阵,为该方格周围延伸 k 长度所围成的数字加和(若超出范围外,则数组外的不算)。比如 2矩阵求和1 + 2 + 3 + 4 + 5 + 6 = 215矩阵求和1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 = 45,求和包括原本的数

💻第一步:

本题是二维前缀和模版的延伸,所以要用到上一篇的两个公式

传送门:【模版】前缀和(二维)

根据题意我们可以延伸该方格周围 k 长度,若超出范围,则让他回到边界上,使用 maxmin 就能解决
在这里插入图片描述
💻第二步:

这也是最重要的一步,因为 dp 的下标是从 1 开始读入数据,mat 和 ans 的下标是从 0 开始读入数据的, 为了能使用同一个公式表示来计算,就需要进行函数映射的操作

在这里插入图片描述
映射关系如图所示

💻代码实现:

#include <iostream>
#include <vector>
using namespace std;

class Solution
{
public:
    vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) 
    {
        int m = mat.size(), n = mat[0].size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));

        for (int i = 1; i <= m; ++i)
        {
            for (int j = 1; j <= n; ++j)
            {
                dp[i][j] = dp[i][j - 1] + dp[i - 1][j] - dp[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }

        vector<vector<int>> ans(m, vector<int>(n));
        for (int i = 0; i < m; ++i)
        {
            for (int j = 0; j < n; ++j)
            {
                int x1 = max(0, i - k) + 1, y1 = max(0, j - k) + 1;
                int x2 = min(m - 1, i + k) + 1, y2 = min(n - 1, j + k) + 1;
                ans[i][j] = dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1];
            }
        }
        return ans;
    }
};

以上就是所有精选的前缀和算法题解析,多写几遍代码,不要死套模版,灵活理解应用能够更好地掌握前缀和算法,整理不易,如有出错希望能够私信博主指出错误!😎

希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

请添加图片描述