前缀和---预处理数组的利器

本文介绍了前缀和的概念及其在一维和二维数组中的计算方法,包括一维和二维前缀和的计算公式,并通过实例展示了如何在题目中应用前缀和的思想进行高效计算,如连续数组、区域和、子数组和等问题的解决策略。
摘要由CSDN通过智能技术生成

目录

一、一维前缀和

二、二维前缀和

三、题目练习

1、一维前缀和模板题---点击跳转题目

2、二维前缀和模板题----点击跳转题目

3、寻找数组中心下标----点击跳转题目

4、除⾃⾝以外数组的乘积----点击跳转题目

5、和为k的⼦数组----点击跳转题目

6、和被k整除的子数组---点击跳转题目

7、连续数组----点击跳转题目

8、矩阵区域的和----点击跳转题目


前缀和(Prefix Sum)是一种在数组或序列上的计算技巧,通过对数组的预处理,可以快速计算给定区间内元素的总和。

一、一维前缀和

前缀和概念:前缀和是指对于一个数组或序列,每个位置的前缀和表示该位置之前所有元素的总和。例如,对于数组 [1,2,3,4,5,6],其前缀和可以表示为 [1,3,6,10,15,21]

我们在使用前缀和时,往往会把数组从下标一开始记录,这样可以避免边界情况的判断

计算公式:prefixSum[i] = prefixSum[i - 1] + element[i]

连续区间[l,r]的和:sum = pre[r] - pre[l-1]

通过前缀和数组的性质,我们可以用递推的方式来得到一个完整的前缀和数组

即当前 i 位置的前缀和等于 i - 1 位置的前缀和加上 i 位置的原数组元素

二、二维前缀和

故名思意,二维前缀和就是对二维数组做预处理

概念:二维前缀和pre[ i ][ j ]表示原数组中从(0,0)到(i,j)对应矩形上所有元素的和

计算公式:pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + src[i][j]

矩阵(x1,y1)到(x2,y2)的区域和公式:

公式看上去有些复杂,让我们来理解一下:

此时我们要求(i,j)位置的前缀和,上图是对原数组划分

也就是:pre[i][j] = 红+蓝+绿+⻩

黄色就是原数组对应位置的元素 src[i][j]

通过我们定义的前缀和的状态,单独求蓝色、绿色不好求,但是我们可以退而求其次,求红+蓝和红+绿,最后再减去多加的一块红色就大功告成了!

红+蓝:也就是从(0,0)到(i-1,j)的元素总和 pre[i-1][j]

红+绿:也就死从(0,0)到(i,j-1)的元素总和 pre[[i][j-1]

红色:通过前缀和的状态定义,pre[i-1][j-1]

红蓝 + 红绿 - 红 + 黄 加也就得到了前缀和的递推公式!

由此我们可以知道,要计算当前位置的前缀和,需要知道左上角、正上方、左侧三个位置的前缀和;求和时按照从上自下,从左至右的顺序即可求得前缀和数组

矩阵区域和也是同样的道理,只是划分对象变成了前缀和数组

三、题目练习

1、一维前缀和模板题---点击跳转题目

要求一段区间的和,通过先对原数组预处理得到前缀和数组后

【l,r】区间的和:pre[r[ - pre[l-1]

代码:

#include <iostream>
using namespace std;

const int N = 1e5 + 10;
long long a[N], pre[N];

int main() 
{
    int n,q;
    cin>>n>>q;
    for(int i=1;i<=n;i++) cin>>a[i];
    
    for(int i=1;i<=n;i++) pre[i] = pre[i-1] + a[i];

    while(q--)
    {
        int l ,r;
        cin>>l>>r;
        cout<<pre[r] - pre[l-1]<<endl;
    }
    return 0;
}

2、二维前缀和模板题----点击跳转题目

求出前缀和数组后,每次对一个矩形区域的求和查询都是O(1)的

以(x1,y1)为左上角,(x2,y2)为右下角的子矩阵的和: pre[x2][y2] - pre[x1-1][y1-1]

代码:

#include <iostream>
using namespace std;

long long a[1010][1010],dp[1010][1010];

int main() 
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n,m,q;
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++) cin>>a[i][j];

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

    int x1,y1,x2,y2;
    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;
}

3、寻找数组中心下标----点击跳转题目

本题我们利用前缀和的思想,重新定义一个前缀和与后缀和

lsum[i] : i 位置之前所有元素的和(不包含i位置)

rsum[i]  i 位置之后所有元素的和(不包含i位置)

做完预处理后我们只需遍历每一个位置判断 lsum[i] 与 rsum[i] 是否相等

代码:

class Solution {
public:
    int pivotIndex(vector<int>& nums) 
    {
        int n = nums.size();
        vector<int> lsum(n+10),rsum(n+10);

        lsum[0] = 0;//初始化,使得循环有初始值可递推
        for(int i=1;i<n;i++)
            lsum[i] = lsum[i-1] + nums[i-1];

        rsum[n-1] = 0;//初始化,同上
        for(int i=n-2;i>=0;i--)
            rsum[i] = rsum[i+1] + nums[i+1];

        for(int i=0;i<n;i++)
        {
            if(lsum[i] == rsum[i])
            {
                return i;
            }
        }
        return -1;
    }
};

4、除⾃⾝以外数组的乘积----点击跳转题目

还是利用前缀和公式的递推思想,结合题意,我们要计算i位置除自身以外的所有元素乘积,只需计算出i位置之前的乘积和i位置之后的乘积,两者再相乘即可

先做预处理:

f[i] : i位置之前的元素乘积(不包含i位置)

g[i]: i位置之后的元素乘积(不包含i位置)

遍历所有位置,结果为:f[i]*g[i]

代码:

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

        f[0] = 1;//初始化,使得循环内能递推,因为是乘积,所以初始为1
        for(int i=1;i<n;i++)
            f[i] = f[i-1]*nums[i-1];

        g[n-1] = 1;
        for(int i=n-2;i>=0;i--)
            g[i] = g[i+1]*nums[i+1];

        vector<int> ans(n);
        for(int i=0;i<n;i++)
        {
            ans[i] = f[i]*g[i];
        }
        return ans;
    }
};

5、和为k的⼦数组----点击跳转题目

要求一段连续区间的和为k,因为数组可能存在0和负数,是不能使用滑动窗口的(和不存在单调性,指针不是同向双指针)

暴力做法就是枚举每一个子区间,计算它们的和来判断是否等于k,也就以每个位置为起点去枚举子数组,时间复杂度是O(n^2),不考虑

由于前缀和的性质是以i为结尾来表示状态,那么我们以i为结尾来分析一下:

要求0到i有多少个子数组和为k,假设x3到i的和为k,0到i的和为sum[i](前缀和),那么0到x2的和为sum-k(前缀和);如果我们从前往后枚举每一个i,要求每个0到i上有多少个和为k的子数组等价于针对每个i,0到i-1区间有多少个和为sum[i]-k的的前缀和

遍历时,每次把计算的前缀和sum[i]存进哈希表中,计算sum[i]时,哈希表中已经存了0到i-1的sum,查询sum[i] - k的个数,就是0到i-1中有多少个满足的子数组

特别要注意的是,当sum[i]=k时,代表此时的0到i区间只有一个子数组满足和为k,此时sum[i]-k=0,哈希表中是没有记录前缀和为0的,我们需要初始化hash[0] = 1

代码:

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) 
    {
        unordered_map<int,int> hash;
        hash[0] = 1;//初始化,前缀和为0时有一个数组
        
        int sum = 0, ret = 0;
        for(auto x : nums)
        {
            sum += x;
            //存在前缀和为sum-k时,把个数添加到结果中
            if(hash.count(sum - k)) ret += hash[sum - k];
            hash[sum]++;
        }
        return ret;
    }
};

6、和被k整除的子数组---点击跳转题目

做本题需要用到的前置知识:

同余定理:如果(a-b)%p 为整数,则a%p = b%p

C++中负数%正数为负数,修正方法:(a%p+p)%p --->此时正负数的结果都正确

思路和上一题一致,从前往后遍历i,求0到i有多少个子数组和被k整除

设x到i和sum被k整除,设0到x-1的和为a,也就是要找(sum-a)%k=0的子数组

由同余定理可得,sum%k=a%k

此时问题就等价于找到针对每个sum[i],0到i-1有多少个前缀和满足sum[i]%k=a%k

创建一个哈希表,每次把sum[i]存进哈希表中,每次查询满足前缀和取模k相等的数组来计数

同样,要注意sum=k时,此时只有一个数组满足,需要初始化

代码:

class Solution {
public:
    int subarraysDivByK(vector<int>& nums, int k) 
    {
        unordered_map<int,int> hash;

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

7、连续数组----点击跳转题目

0和1的数量相等的子数组,我们变一下形,把所有的0变成-1,题目就转化成了求和为0的最长子数组,和上面两题很类似,只是此时哈希表中不再存个数而是存<前缀和,对应下标>

同样的,初始化问题,当sum[i]=0时,代表0到i就是满足的数组,此时如何计算长度呢?hash[0]=-1,此时i -(-1)就是当前数组的长度

代码:

class Solution {
public:
    int findMaxLength(vector<int>& nums) 
    {
        for(auto& x : nums)
        {
            if(x == 0) x = -1;
        }

        unordered_map<int,int> hash;
        hash[0] = -1;//默认有一个前缀和为0的,针对以i结尾的前缀和为0,此时长度为以i结尾的长度
        int len = 0, n = nums.size(),sum = 0;
        for(int i=0;i<n;i++)
        {
            sum += nums[i];
            if(hash.count(sum))  len = max(len,i - hash[sum] );
            else hash[sum] = i;
        }
        return len;
    }
};

8、矩阵区域的和----点击跳转题目

题目要求返回一个ans矩阵,ans矩阵的每个元素是mat矩阵中一个子矩阵的和,所以需要快速查询mat矩阵中子矩阵的和,需要用到二维前缀和

本题因为题目给的原数组是从下标0开始的,但是我们在计算前缀和为了避免边界情况都是从下标1开始,所以需要注意下标的映射关系,返回的ans数组要求和原数组大小相等,也就是也从下标0开始记录,在查询前缀和数组填表时也要注意下标的映射关系

代码:

class Solution {
public:
    vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) 
    {
        int m = mat.size();
        int n = mat[0].size();
        //二维vector的定义
        vector<vector<int>> ans(m,vector<int>(n));
        vector<vector<int>> pre(m+1,vector<int>(n+1));

        //从1开始,避免边界问题
        for(int i=1;i<=m;i++)
            for(int j=1;j<=n;j++)
            pre[i][j] = pre[i-1][j]+pre[i][j-1]-pre[i-1][j-1] + mat[i-1][j-1];

        
        for(int i=0;i<m;i++)
            for(int j=0;j<n;j++)
            {
                //使用前缀和数组,下标映射,+1
                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] = pre[x2][y2] - pre[x1 - 1][y2]
                           - pre[x2][y1 - 1] + pre[x1 - 1][y1 - 1];

            }
            return ans;
    }
};

创建前缀和数组时pre中(i,j)位置的值需要到mat中的(i-1,j-1)查找

创建ans数组时,ans中(i,j)位置的值需要到pre中的(i+1,j+1)查找

通过以上题目得练习,我们会发现,前缀和更多得是一种思想,提前对数组预处理可以优化后续的解题;或者是很多题目并不是考察前缀和本身,而是需要我们理解前缀和的递推思想,因地制宜地根据题目来改写前缀和

  • 21
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值