【LeetCode热题100】前缀和

这篇博客共记录了8道前缀和算法相关的题目,分别是:【模版】前缀和、【模版】二维前缀和、寻找数组的中心下标、除自身以外数组的乘积、和为K的子数组、和可被K整除的子数组、连续数组、矩阵区域和。

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

int main() 
{
    //1. 读取数据
    int n = 0, q = 0;
    cin >> n >> q;
    vector<int> nums(n+1);
    vector<long long> dp(n+1);
    for(int i = 1; i<= n ;i++)
    {
        cin >> nums[i];
        //2. 预处理出来一个前缀和数组
        dp[i] = dp[i-1] + nums[i];
    }

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

   return 0; 
}

题目分析:首先来看暴力解法,题目让我们求l到r的和,我们可以找到l,然后依次往后累加,直到加到r,然后执行q次,所以这种暴力解法的时间复杂度是O(N*q)。这种暴力解法肯定过不了。所以,我们要使用前缀和方法解决这个问题。所谓前缀和,就是快速求出数组中某一个连续区间的和。快速:使用O(1)的时间复杂度找到一次结果。我们从题目中看到,数组元素下标是从1开始的,下标为0的位置我们默认为0。具体来说,第一步:预处理出来一个前缀和数组dp,其中dp[i]表示[1,i]区间内所有元素的和,dp[0]默认设为0,dp[i]=dp[i-1]+arr[i]。第二步:使用前缀和数组。当我们想求[l, r]区间的和时,可以直接用dp[r]-dp[l-1]求解。这样直接求解的时间复杂度为O(1)。

细节问题:为什么我们的下标要从1开始计数?为了处理边界情况。如果下标从0开始,比如,要求下标0-2之间元素之和,按照上面的方法就是求dp[2]-dp[-1],明显越界了,对于这种情况,还需要单独处理。

#include <iostream>
#include <vector>
using namespace std;
 
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][j-1] + dp[i-1][j] + arr[i][j] - dp[i-1][j-1];
        }
    }
    //3.利用前缀和矩阵
    int x1 = 0, x2 = 0, y1 = 0, y2 = 0;
    while(q--)
    {
        cin >> x1 >> y1 >> x2 >> y2 ;
        cout << dp[x2][y2] - dp[x2][y1-1] - dp[x1-1][y2] + dp[x1-1][y1-1] << endl;
    }
}

题目分析:最开始我们想到的肯定是暴力求解,但是很明显暴力求解的时间复杂度很高,是O(n*m*q),所以我们再来说一种更好的解法--前缀和,第一步,我们预处理出来一个前缀和矩阵dp,dp[i][j]表示从[1][1]位置到[i][j]位置这段区间里面所有元素的和。第二步,使用前缀和矩阵。具体计算过程如下:

class Solution {
public:
    int pivotIndex(vector<int>& nums) 
    {
        //1.先创建前缀和数组f,后缀和数组g
        int n = nums.size();
        vector<int> f(n);
        vector<int> g(n);
        for(int i = 1;i<n;i++)
        {
            f[i] = f[i-1] + nums[i-1];
        }
        for(int j = n-2 ; j >= 0 ; j--)
        {
            g[j] = g[j+1] + nums[j+1];
        }
        //2.使用前缀和数组和后缀和数组
        for(int i = 0 ; i < n ; i++)
        {
            if(f[i] == g[i]) return i;
        }
        return -1;

    }
};

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) 
    {
        int n = nums.size();
        vector<int> f(n);
        vector<int> g(n);
        vector<int> ret(n);
        f[0] = 1,g[n-1] = 1;
        //1.先求出前缀积和后缀积数组
        for(int i = 1 ;i < n ; i++ )
        {
            f[i] = f[i-1] * nums[i-1];
        }
        for(int j = n-2 ;j >= 0 ; j-- )
        {
            g[j] = g[j+1] * nums[j+1];
        }
        //2.使用前缀和数组
        for(int i = 0; i < n ; i++)
        {
            ret[i] = f[i] * g[i];
        }

        return ret;
    }
};

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) 
    {
        unordered_map<int,int> hash; // 统计前缀和出现的次数
        int sum = 0, ret = 0;
        hash[0] = 1;
        for(auto c : nums)
        {
            sum += c; //计算当前位置的前缀和
            if(hash.count(sum - k)) ret += hash[sum - k]; //统计个数
            hash[sum]++;
        }
        return ret;
    }
};

题目分析:对于这道题,如果使用暴力求解,其时间复杂度是O(N2),所以我们换一种方法,我们使用前缀和的思想,在以i为结尾的所有子数组中找,假设以i为结尾的子数组前缀和是sum[i],那么就是在[0,i-1]区间中,有多少个前缀和为sum[i]-k。在找有多少个前缀和为sum[i]-k时,我们可以将之前的前缀和放到一个哈希表中,哈希表的key是前缀和,value前缀和的次数。其实我们不需要搞出一个前缀和数组sum[i],而只需有一个变量sum就行,sum是前i个元素的和。

有几个细节需要处理:1.前缀和加入哈希表的时机:在计算i位置之前,哈希表里只保存[0,i-1]位置的前缀和。2.不用真的创建一个前缀和数组,用一个变量sum来标记前一个位置的前缀和即可。3.如果整个前缀和等于k呢,其实我们应该在开始时设置hash[0]=1。

class Solution 
{
public:
    int subarraysDivByK(vector<int>& nums, int k) 
    {
        unordered_map<int, int> hash;
        int sum = 0;
        int reminder = 0;
        int ret = 0;
        hash[0] = 1; // 0这个数的余数
        for(auto e : nums)
        {
            sum += e;  // 当前位置的前缀和
            reminder = (sum % k + k)%k; //修正后的余数
            if(hash.count(reminder)) ret += hash[reminder]; //统计结果
            hash[reminder]++;
        }
        return ret;
    }
};

题目分析:对于这道题,我们先要补充两个知识:1.同余定理,(a-b)/p = k,可以得出a%p=b%p。2.C++/JAVA中,负%正 = 负,修正-->(a%p+p)%p,这样得到的结果就是一个正确的正数。好了,有了这两个补充知识,剩下的就和上道题几乎一样了。

class Solution {
public:
    int findMaxLength(vector<int>& nums) 
    {
        unordered_map<int,int> hash;
        int sum = 0;
        hash[0] = -1; //默认有一个前缀和为0的情况
        int 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;

    }
};

题目分析:这道题如果直接求会比较困难,我们可以转化一下,将所有的0改为-1,问题就转化为在数组中,找出最长的子数组,使子数组中所有元素的和为0。这样就和之前和为k的子数组的做法很像,也是使用前缀和+哈希表。但是还是有一些区别,1.哈希表中存什么呢?hash<int,int>的第一个int应该是前缀和,第二个int应该是下标。2.什么时候存入哈希表?使用完之后,丢进哈希表。3.如果有重复的<sum,i>,如何存?只保留前面的那一对<sum,i>。4.默认的前缀和为0的情况,如何存?hash[0]=-1。5.长度怎么算?i-j。

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];
            }
        }
        //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,x2 = min(m-1 , i+k)+1,y2 = min(n-1,j+k)+1;
                ret[i][j] = dp[x2][y2] - dp[x2][y1-1] - dp[x1-1][y2] + dp[x1-1][y1-1];
            }
        }
        return ret;
    }
};

题目分析:这道题很明显需要用到二维数组前缀和,首先,我们要预处理得到一个前缀和数组dp,dp元素的求法如下图(先以mat的起始下标为(1,1)为例):

得到前缀和数组之后,假设我们要求(x1,y1)~(x2,y2)区间元素的和,其算法如下图:

按照题目要得到的矩阵,anwser[i][j]所求的就是[i-k][j-k]~[i+k][j+k]区间元素的和,就可以使用上面求ret的方法,但是i-k、j-k、i+k、j+k有可能越界,所以我们不得不考虑这些越界情况:

但是,这道题的mat的其实下标是从0开始的,我们就必须考虑下标的映射关系,dp数组必须要多加一行多加一列,然后dp就可以从[1,1]下标开始,所以,在求dp矩阵时,在找原始矩阵mat时下标要-1。然后在使用dp表求ans矩阵时,要对下标+1,这其实可以在我们计算(x1,y1)~(x2,y2)时,就给下标+1。

评论 55
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值