C++算法:前缀和

前缀和

1.(模板)一维前缀和

前缀和,能快速求出数组arr中某一个连续区间的和 。设arr=[1,4,7,2,5,8,3,6,9]。创建前缀和数组dp,dp中每个元素dp[i]为arr在[1,i]区间的元素和。即 dp[i]=dp[i-1]+arr[i] 。数组的下标应从1开始,调用vector创建数组时要开n+1个空间,arr[0]和dp[0]都会被初始化为0 。

求arr中[l,r]的元素和。如下图所示,先得到1~r的元素和dp[r], 再得到1~l-1的元素和dp[l-1],二者做差即[l,r]的元素和。

l~r元素和 = dp[r] - dp[l-1]

若下标从0开始,如果求[0,2]元素和,则dp[2]-dp[-1],此时要处理边界问题,不然会越界访问。下标从1开始则不会有这个问题,例如求[1,2]元素和,则dp[2]-dp[0],dp[0]为0,元素和即为dp[2] 。

根据题意,查询m次元素和,直接遍历,即按照描述操作,时间复杂度为O(m*n),若用前缀和,时间复杂度为O(m+n) 。

cpp 复制代码
#include <iostream>
#include<vector>
using namespace std;

int main() 
{
    int n=0,m=0;
    cin>>n>>m;
    vector<int> arr(n+1);
    for(int i=1;i<=n;i++)
        cin>>arr[i];
	
    //预处理一个前缀和数组
    vector<long long> dp(n+1);//防溢出
    for(int i=1;i<=n;i++)
        dp[i]=dp[i-1]+arr[i];
    
    int l=0,r=0;
    while(m--)
    {
        cin>>l>>r;
        cout<<dp[r]-dp[l-1]<<endl;
    }

    return 0;
}

2.(模板)二维前缀和

下标从1开始的矩阵arr,给左上角元素arr[x1] [y1],右下角元素arr[x2] [y2],求这个范围内的元素和。

开一个与arr同样规模的前缀和矩阵dp,dp[i] [j]为从arr[1] [1]到arr[i] [j]的元素和。

dp[i] [j] = A区域元素和 + B区域元素和 + C区域元素和 + D 。即dp[i] [j] = (A+B)+(A+C)+D-A。

dp[i][j] = dp[i-1][j] + dp[i][j-1] + arr[i][j] - dp[i-1][j-1]

对于给定左上角元素arr[x1] [y1],右下角元素arr[x2] [y2],求这个范围内的元素和。

即求D区域前缀和,D区域前缀和 = [1,1]到[x2,y2]前缀和 - [1,1]到[x1-1,y2]前缀和 - [1,1]到[x2,y1-1]前缀和 + [1,1]到[x1-1,y1-1]前缀和。即D = (A+B+C+D) - (A+B) - (A+C) + A 。

[x1,y1]到[x2,y2]前缀和 =dp[x2,y2] - dp[x1-1,y2] - dp[x2,y1-1] + dp[x1-1][y1-1]

查询q次,arr为nm矩阵,直接暴力解法的时间复杂度为O(n * m * q),前缀和时间复杂度为O(q + nm)。

cpp 复制代码
int main()
{
    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];

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

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

3.寻找数组的中心下标

题目要求返回一个下标 i ,满足0 ~ i-1的元素和等于 i+1 ~ n-1的元素和。使用前缀和时不一定要套模板,关键是能仿照前缀和的思想解题。之前的前缀和dp[i]是下标从1开始到 i 的元素和,现在可以定义前缀和 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]。

f[0]为下标为0的元素之前的元素和,对于数组nums是越界的,所以规定 f[0]=0;同理g[n-1]也会越界访问nums,所以规定 g[n-1]=0 。

cpp 复制代码
class Solution {
public:
    int pivotIndex(vector<int>& nums)
    {
        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 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;
    }
};

4.除自身以外数组的乘积

与上一道题类似,参照前缀和的思想,定义与上一题类似的前缀积、后缀积,注意处理边界情况。

cpp 复制代码
class Solution3 {
public:
    vector<int> productExceptSelf(vector<int>& nums)
    {
        int n = nums.size();
        vector<int> f(n), g(n), answer(n);
        
        //预处理前缀积
        f[0] = 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];

        for (int i = 0;i < n;i++)
            answer[i] = f[i] * g[i];
        return answer;
    }
};

5. 和为k的子数组

题目要求返回和为k的子数组的个数,nums[i]是可以为负数的,所以不能用滑动窗口。如果子数组[j,i]元素和为k,i的前缀和为sum,则j的前缀和为sum-k。

题目可以转换成 i 的前缀和为sum,记录0到 i-1 的每个前缀和,如果0到 i-1 中存在前缀和为sum-k,就相当于找到一个符合要求的子数组。用哈希表记录前缀和,key为前缀和,value为 值为key的前缀和个数。前缀和为sum-k,个数ret+=hash[sum-k]。

不必再创建一个前缀和数组,遍历nums,用sum记录前缀和,哈希表保存sum。如果nums[0]==k,则一开始sum为k,sum-k就为0,所以要预处理hash[0]=1。得到0到 i 前缀和sum,先调用hash.count查找是否有前缀和满足sum-k,然后再把sum保存到哈希表。

cpp 复制代码
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 e : nums)
        {
            sum += e;
            if (hash.count(sum - k))
                ret += hash[sum - k];
            hash[sum]++;
        }
        return ret;
    }
};

6. 和可被k整除的子数组(同余定理)

与上一道题类似。先补充同余定理和C++、Java关于 负数%正数。

同余定理:(a-b)/p = k ... 0可得出a%p = b%p。证明:因为(a-b)/p=k,所以a-b=kp,即a=b+kpa%p的本质是a一直减p ,直到第一次结果x小于p,x即为a/p的余数,即x=a%p。又因为a=b+kp,同时取模,即a%p=(b+kp)%p,则(b+kp)%pb%p,即a%p=b%p

C++、Java中 负数%正数得到负数,写代码时要修正,无论a为正数还是负数,a%p都写成(a%p+p)%p,结果就都为正数。

nums中,[j,i]的元素和 % k==0,则 ( i的前缀和 - j的前缀和 )%k == 0,根据同余定理,i的前缀和 % k == j的前缀和 % k。设 i 的前缀和为sum,只要 i 之前存在前缀和x,满足 x%k == sum%k,即找到了符合要求的子数组。

创建unordered_map<int,int> hash,key为 前缀和%k所得到的余数,value为该余数的个数。如果nums[0]%k==0,则一开始sum除以k的余数为0,预处理hash[0]=1。

cpp 复制代码
class Solution {
public:
    int subarraysDivByK(vector<int>& nums, int k)
    {
        unordered_map<int, int> hash;
        hash[0] = 1;//0的余数

        int sum = 0, ret = 0;
        for (auto e : nums)
        {
            sum += e;//当前位置的前缀和
            int r = (sum % k + k) % k;
            if (hash.count(r))
                ret += hash[r];//统计结果
            hash[r]++;
        }
        return ret;
    }
};

7.连续数组

题目要求找到0和1个数相同的最长子数组,把0改成-1,题目就转化成找到和为0的最长子数组,即在nums中,在[0,i]中找到[0,j]的元素和为sum。与和为k的子数组类似。

创建unordered_map<int,int> hash,key为前缀和,因为要求出长度,所以value为下标。先得到前缀和sum,调用hash.count,在哈希表中找sum,计算出长度。

如何计算长度?i-j

hash[0]=-1,当前缀和sum首次为0时,如果下标为0,则长度 i-0=i,而长度应该为 i+1。

只有在hash.count未找到前缀和时,才需要把前缀和及其下标插入哈希表。如果得到的每一个<sum,i >都插入哈希表,就会导致上图的 j 右移,i-j的长度不是最长。

如果不这样做,sum为0时,就会从<0,-1>变成<0,1>,最终i为3时,i-(-1)变成 i-1,长度变短。

cpp 复制代码
class Solution {
public:
    int findMaxLength(vector<int>& nums)
    {
        int n = nums.size();
        unordered_map<int, int> hash;
        hash[0] = -1;

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

8.矩阵区域和

题目的意思是返回一个与mat同样规模的矩阵ans,ans[i] [j]为mat[i] [j]向四周扩展k范围的元素和,下图k=1,ans[0] [0] = mat[0] [0]+mat[0] [-1]+mat[0] [1] + mat[-1] [0]+mat[1] [0] + mat[-1] [-1]+mat[-1] [1]+mat[1] [-1]+mat[1] [1] = 12 。

这道题显然用二维前缀和来解决。创建前缀和矩阵dp,mat下标是从0开始的,所以往dp填入数据的公式应为:

dp[i][j] = dp[i][j-1] + dp[i-1][j] - dp[i-1][j-1] + mat[i-1][j-1]

ans中的数据本质是mat中的元素和,得到mat某一范围的元素和,需要该范围左上角和右下角的元素下标,设ans[i] [j]对应mat中的范围左上角元素mat[x1] [y1],右下角的元素mat[x2] [y2]。即

ans[i][j] = dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1]

ans[i] [j]对应mat[i] [j]向四周扩展,为了防止越界,mat[x1] [y1]即mat[ max(0, i-k) ] [ max(0, j-k) ],mat[x2] [y2]即mat[ min(m-1, i+k) ] [ min(n-1, j+k) ],对应到dp,由于dp下标从1开始,x1、y1、x2、y2都要 +1。

cpp 复制代码
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)), ans(m, vector<int>(n));
        for (int i = 1;i < m + 1;i++)
            for (int j = 1;j < n + 1;j++)
                dp[i][j] = dp[i][j - 1] + dp[i - 1][j] - dp[i - 1][j - 1] + mat[i - 1][j - 1];

        for (int i = 0;i < m;i++)
        {
            for (int j = 0;j < n;j++)
            {
                int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
                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;
                ans[i][j] = dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1];
            }
        }
        return ans;
    }
};
相关推荐
隔壁大炮1 小时前
Day07-词嵌入层解释
人工智能·深度学习·算法·计算机视觉·cnn
啊我不会诶1 小时前
Codeforces Round 1091 (Div. 2) and CodeCraft 26
c++·算法
H Journey1 小时前
常用知识总结C++、CMake、Linux
linux·c++·opencv·cmake
凌波粒1 小时前
LeetCode--二叉树前中后序遍历的递归与迭代实现(二叉树/DFS)
算法·leetcode·深度优先
啊哦呃咦唔鱼1 小时前
Leetcodehot100-215. 数组中的第K个最大元素
数据结构·算法·leetcode
cany10002 小时前
C++ -- 宏和模板
开发语言·c++
初心未改HD2 小时前
Go语言接口与nil深度解析
开发语言·golang
老赵聊算法、大模型备案2 小时前
从剪映、即梦 AI 被罚,读懂 AI 生成内容标识硬性合规要求
人工智能·算法·安全·aigc
Achou.Wang2 小时前
go语言并发编程
java·开发语言·golang