前缀和算法

目录

牛客/leetcode题目

一、【模板】一维前缀和

二、【模板】二维前缀和

三、寻找数组的中心下标

四、除自身以外数组的乘积

五、和为k的子数组

六、和可被K整除的子数组

七、连续数组

八、矩阵区域和


牛客/leetcode题目

一、【模板】一维前缀和

【模板】前缀和_牛客题霸_牛客网 (nowcoder.com)icon-default.png?t=N7T8https://www.nowcoder.com/practice/acead2f4c28c401889915da98ecdc6bf?tpId=230&tqId=2021480&ru=/exam/oj&qru=/ta/dynamic-programming/question-ranking&sourceUrl=%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587%26topicId%3D1961.题目解析

1.第一行输入n和q, 表示数组元素个数和q次询问

2.第二行输入数组的n个元素

3.剩下的q行每行输入两个数,表示起始位置和终止位置

4.求数组从起始位置和终止位置这段区间的和

注意:数组的下标从1开始

 2.算法分析

解法一:暴力解法

简单模拟题目要求, 每次询问把要求区间的元素遍历求和

时间复杂度: O(q*n)

解法二:前缀和

前缀和用于解决快速求数组中某一个连续区间的和的问题

时间复杂度: O(q)

前缀和算法步骤:

1.预处理出来一个前缀和数组 dp (和原始数组大小一样)

dp[i]表示 arr 数组 [1, i] 区间内所有元素的和

结论: dp[i] = dp[i-1] + arr[i]

2.使用前缀和数组

如果题目要求的是 arr数组 [l, r] 的和, 那么我们可以间接求,用[1, r]区间的和 减去 [1, l-1]区间的和求实[l, r]区间的和, 而[1, r]区间的和是dp[r], [1, l-1]的和是 dp[l-1], 而dp数组我们已经预处理好了,因此求和只需要O(1)的时间复杂度

而如果是q次询问,最终时间复杂度是 O(n)+O(q)

细节问题:为啥数组的下标最好从1开始?

·如果从0开始计数,要求的是[0, 2]区间的和, 那么 结果就是 dp[2] - dp[-1], 发生了越界访问, 需要特殊处理一下边界情况!

·如果从1开始计数,求的是[1, 2]区间的和, 那么 结果就是 dp[2] - dp[0], 在预处理数组时只需要让dp[0] = 0 即可, 求出来的就是dp[2],也就是[1, 2]区间的和

3.算法代码

#include <iostream>
using namespace std;
#include<vector>
int main() 
{
    //1.读入数据
    int n, q;
    cin >> n >> q;
    vector<int> arr(n+1, 0);
    for(int i = 1; i <= n; i++)
        cin >> arr[i];

    //2.预处理前缀和数组
    vector<long long> dp(n+1); //防溢出
    for(int i = 1; i <= n; i++)
    {
        dp[i] = dp[i - 1] + arr[i];
    }

    //3.使用前缀和数组
    int l = 0, r = 0;
    while(q--)
    {
        cin >> l >> r;
        cout << dp[r] - dp[l-1] << endl;
    }
    return 0;
}

二、【模板】二维前缀和

【模板】二维前缀和_牛客题霸_牛客网 (nowcoder.com)icon-default.png?t=N7T8https://www.nowcoder.com/practice/99eb8040d116414ea3296467ce81cbbc?tpId=230&tqId=2023819&ru=/exam/oj&qru=/ta/dynamic-programming/question-ranking&sourceUrl=%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587%26topicId%3D1961.题目解析

给定一个n行m列的矩阵,q次询问,每次询问输入4参数,代表(x1, y1), (x2, y2), 输出以(x1, y1)为左上角,以(x1, y1)为右下角的子矩阵的和

2.算法分析

解法一:暴力解法

每次询问,老老实实把要求的矩阵和遍历一遍求和即可 --- O(n*m*q)

解法二:前缀和解法

1.预处理出来一个前缀和矩阵 dp (和原始矩阵大小一样)

dp[i][j]表示从 arr矩阵中从 [1, 1]到 [i, j] 这个子矩阵的所有元素的和

求解dp矩阵:

2.使用前缀和矩阵

总的时间复杂度: O(m*n) + O(q)

细节问题:为啥下标从[1, 1]开始,原因和一维前缀和模板中的理由一样,此处就不赘述了~

 3.算法代码

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

int main() 
{
    //1.读入数据
    int n = 0, m = 0, q = 0;
    cin >> n >> m >> q;
    vector<vector<int>> arr(n+1, vector<int>(m+1));
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            cin >> arr[i][j];

    //2.预处理前缀和矩阵
    vector<vector<long long>> dp(n+1, vector<long long>(m+1));
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            dp[i][j] = dp[i-1][j] + dp[i][j-1] + arr[i][j] - dp[i-1][j-1];

    //3.使用前缀和矩阵
    int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
    while(q--)
    {
        cin >> x1 >> y1 >> x2 >> y2;
        cout << (dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1]) << endl;
    }
    return 0;
}

三、寻找数组的中心下标

724. 寻找数组的中心下标 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/find-pivot-index/1.题目解析

1.求数组中心下标
2.中心下标是指该下标的左侧元素之和等于该下标的右侧元素之和
3.中心下标在左端,左侧元素和是0, 中心下标在最右边,右侧元素和是0
4.有多个中心下标,返回最左边的一个

2.算法分析
解法一: 暴力解法
每次枚举一个下标,求出该下标左侧元素之和和右侧元素之和 --- O(N^2)
解法二: 前缀和思想

f表示前缀和数组,g表示后缀和数组
f[i] 表示 [0, i-1] 区间,所有元素的和     ---》   f[i] = f[i-1] + nums[i-1]
g[i] 表示 [i+1, n-1] 区间,所有元素的和   ---》  g[i] = g[i+1] + nums[i+1]
注意: 前缀和(后缀和)数组的表达式就提论题,此题要求的是i之前的所有元素之和 与 i之后的所有元素之和,因此不包含nums[i], 所以f[i]和g[i]的含义较上道题目有所变化

因此判断 i 是否是中心下标只需要看 f[i] 是否 == g[i] 即可

细节处理:

1. f[0]=0, g[n-1]=0 

2. f表从左向右填,g表从右向左填

3.算法代码 

class Solution {
public:
    int pivotIndex(vector<int>& nums) 
    {
        int n = nums.size();
        //1.预处理前缀和、后缀和数组
        vector<int> f(n); //vector默认值就是0
        vector<int> g(n); //vector默认值就是0
        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];

        //2.使用前缀和、后缀和数组
        for(int i = 0; i < n; i++)
            if(f[i] == g[i]) return i;
        return -1;
    }
};

四、除自身以外数组的乘积

238. 除自身以外数组的乘积 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/product-of-array-except-self/1.题目解析

求每个元素除自身之外其余所有元素的乘积

2.算法分析

前缀积  f[i] 表示[0, i-1] 区间内所有元素的乘积  f[i] = f[i-1] * nums[i-1]

后缀积 g[i] 表示[i+1, n-1]区间内所有元素的乘积  g[i] = g[i+1] * nums[i+1]

细节问题:

1. f[0]=1, g[n-1]=1

2. f表从左向右填,g表从右向左填

3.算法代码

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums)
    {
        int n = nums.size();
        vector<int> f(n);
        vector<int> g(n);

        //1.预处理前缀积和后缀积数组
        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];

        //2.使用前缀积和后缀积数组
        vector<int> ret(n);
        for(int i = 0; i < n; i++)
            ret[i] = f[i] * g[i];
        return ret;
    }
};

五、和为k的子数组

560. 和为 K 的子数组 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/subarray-sum-equals-k/1.题目解析

数组中元素可正可负可为零,  返回和为k的子数组(连续)的个数

2.算法分析

暴力解法:

每次从前向后固定一个下标,然后向后枚举,找到了和为k的子数组,不能停,继续向后枚举,因为数组中元素可正可负可为零,因此每次固定一个下标后从该下标枚举到数组结尾!

不能用滑动窗口算法的原因:

如图所示,假如现在[left, right]区间所有元素之和等于k, 而黄色部分和蓝色部分元素之和恰好等于0, 则中间白色部分元素之和也就等于k, 因此如果right不回退,就会错过一些正确答案,因此不能使用滑动窗口算法~

前缀和算法:

我们只需要遍历原数组一遍,每次统计出 i 位置为结尾的 和等于k的 子数组的 个数即可

细节问题:

1.在计算 i 位置之前, 哈希表中只保存[0, i-1]区间的前缀和

2.不用真正创建一个前缀和数组, 因为dp[i]=dp[i-1]+nums[i],因此只要用一个变量sum标记i位置之前的前缀和即可, 计算出dp[i]之后,更新sum即可

3.有可能以i位置为结尾的数组中, 整个数组的和为k

因此我们提前把 前缀和0 和个数1建立映射关系

3.算法代码

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;
    }
};

六、和可被K整除的子数组

974. 和可被 K 整除的子数组 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/subarray-sums-divisible-by-k/1.题目解析

返回数组中 和 可被K整除的子数组(连续)的个数

2.算法分析

暴力解法和题目五基本是一样的~, 同样也不能用滑动窗口解决问题~

前缀和算法:

补充:

1.同余定理 (a-b) ÷ p = k  ->  a % p == b % p

2.C++中 负数 % 正数 结果是负数,因此我们如果想把结果变为正数 可以采取如下做法:

a % p  ---> a % p + p   但是为了正数也能适用前面的式子,因此 (a % p + p) % p

前缀和思想和题目五也基本是一样的~

本题所采用的哈希表 存储的就是 前缀和的余数 和 个数 的映射关系

3.算法代码

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;
    }
};

七、连续数组

525. 连续数组 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/contiguous-array/1.题目解析

题目要求找出0和1数量相等的最长连续子数组

转化:

1.将所有的0修改成-1

2.在数组中,找出最长的子数组,使子数组中所有元素的和为0

于是本题就转化成了题目五求和为k的子数组的问题了~

2.算法分析

本题与第五题的不同,也就是要注意的细节问题:

1.本题要求的是最长连续子数组的长度,不关心个数,因此哈希表中映射的是前缀和和下标j

2.如果哈希表中加入元素时发现已经有相同的前缀和了, 不需要更新,只需要保留前面的那一对<sum-k, j>, 因为题目求的是最长的连续子数组,因此不需要更新!

3. 前缀和为0的情况,存储的是 hash[0] = -1 (因为下标是-1)

4.和为0的子区间的长度

3.算法代码

class Solution {
public:
    int findMaxLength(vector<int>& nums) 
    {
        unordered_map<int, int> hash;
        hash[0] = -1; //默认有1个前缀和为0的情况
        
        int sum = 0, ret = 0;
        for(int i = 0; i < nums.size(); i++)
        {
            sum += nums[i] == 0 ? -1 : 1; //将数组0变-1, 求前缀和
            if(hash.count(sum)) ret = max(ret, i-hash[sum]); //hash[sum] == hash[sum-0]就是下标j
            else hash[sum] = i; //sum不存在时,才将sum加入哈希表
        }
        return ret;
    }
};

八、矩阵区域和

1314. 矩阵区域和 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/matrix-block-sum/description/1.题目解析

2.算法分析

本题显然是题目二的应用,也就是二维前缀和思想

①ans[i][j]对应的要求和的区域在mat矩阵中是: [i-k, j-k] ~ [i+k, j+k]

但是要注意越界问题, 所以可以这么写:

x1 = max(0, i-k) ; y1 = max(0, j-k)

x2 = min(m-1, i+k) ; y2 = min(n-1, j+k)

②下标的映射关系

·dp矩阵下标是从(1, 1)开始,但原始矩阵mat下标从(0, 0)开始的,因此当我们要预处理

dp[x][y] 的时候,对应的mat矩阵的位置应该是 mat[x-1][y-1] 位置

·最终返回的ans数组下标也是从(0, 0)开始的,因此通过二维前缀和矩阵dp求解ans矩阵时,也要注意映射关系; 当我们求ans[x][y]位置时,对应的dp矩阵的位置应该是dp[x+1][y+1]位置

3.算法代码

class Solution 
{
public:
    vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k)
    { 
        int m = mat.size(), n = mat[0].size();
        //1.预处理前缀和矩阵
        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-1][j] + dp[i][j-1] - dp[i-1][j-1] + mat[i-1][j-1]; //注意mat的下标
        
        //2.使用前缀和矩阵
        vector<vector<int>> ret(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;
                ret[i][j] = dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1];
            }
        return ret;
    }
};

  • 14
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值