算法篇:前缀和

简介

核心思想是:提前计算出从起点到每个位置的累加和,这样当需要求某个区间的和时,不需要遍历区间内的每个元素,直接用两个前缀和相减即可得到,时间复杂度从 O(n) 降到 O(1)

实例

【模板】前缀和

【模板】前缀和_牛客题霸_牛客网

时间限制:1秒 空间限制:256M

题目解析:求规定范围内的和

两种解题方法:

方法一:暴力解法(超时)

直接每次都从l位置的值加到r位置

复制代码
#include <iostream>
using namespace std;
int n,m;
const int N=1e5+10;
long long arr[N]={0};
int main() {
   cin>>n>>m;
   for(int i=1;i<=n;i++)
   {
        cin>>arr[i];
   } 
   for(int i=0;i<m;i++)
   { 
        int l,r;
        cin>>l>>r;
        long long sum=0;
        for(int i=l;i<=r;i++)
        {
            sum+=arr[i];
        }
        cout<<sum<<endl;
   }
}
// 64 位输出请用 printf("%lld")

方法二:前缀和

定义一个数组来保存从1下标开始到i下标的和,这样求l到r的和就转变成了dp[r]-dp[l-1],时间复杂度从O(n)降到了O(1)

复制代码
#include <iostream>
using namespace std;
int n,m;
const int N=1e5+10;
long long arr[N]={0};
long long dp[N]={0};
//表示1到i的和
int main() {
   cin>>n>>m;
   dp[0]=0;
   for(int i=1;i<=n;i++)
   {
        cin>>arr[i];
        dp[i]=dp[i-1]+arr[i];
   } 
   for(int i=0;i<m;i++)
   { 
        int l,r;
        cin>>l>>r;
        cout<<dp[r]-dp[l-1]<<endl;
   }
}

【模板】二维前缀和(medium)

【模板】二维前缀和_牛客题霸_牛客网

题目解析:以输入中1 1 2 2为例,在三行四列的数组中找到从坐标(1,1)开始到(2,2)区域内的和,即1+2+3+2=8.

解题思路:

定义一个能保存从左边(1,1)到(i,j)区域和的数组dp[N][N]。

从图中我们可以看出(1,1)到(i,j)区域=红+蓝+紫+绿区域,而我们已知红色区域、红色+蓝色区域、红色+紫色区域,所以我们要想求上面那一块区域可以用(红色+蓝色区域)+(红色+紫色区域)+绿色区域-红色区域(前面有两块红色区域)

dp[i][j]=dp[i][j-1]+dp[i-1][j]+arr[i][j]-dp[i-1][j-1](细节问题:题目中的坐标从(1,1)开始)

结果处理:输入要求的坐标 x1,y1,x2,y2;

根据上图:我们要求的区域是绿色区域,我们已知红色区域、红色+蓝色区域、红色+紫色区域和全部颜色区域的和,所以我们可以用全部区域-(红+蓝)-(红+紫)+红。

result=dp[x2][y2]-dp[x2][y1-1]-dp[x1-1][y2]+dp[x1-1][y1-1]

细节部分:数组中的值在±10^9范围内,而int类型在±2*10^9范围内,也就是说如果用int来保存dp中的值是不够的(两个10^9就会超出范围),所以用long long.

复制代码
#include <iostream>
using namespace std;
const int N = 1e3 + 10;
int n, m, q;
int arr[N][N] = {0};
long long dp[N][N] = {0};
int x1,x2,y1,y2;
int main() {
    cin >> n >> m >> q;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cin >> arr[i][j];
        }
    }
    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];
        }
    }
    for(int i=0;i<q;i++){
        cin>>x1>>y1>>x2>>y2;
        cout<<dp[x2][y2]-dp[x1-1][y2]-dp[x2][y1-1]+dp[x1-1][y1-1]<<endl;
    }
}

寻找数组的中心下标(easy)

724. 寻找数组的中心下标 - 力扣(LeetCode)

题目解析:找到一个下标,使它左边的和等于右边的和。

这里可以用到前缀和,可以套用一维数组的模板,先求下标0开始到i下标的所有和(dp[i]=dp[i-1]+nums[i]),根据题意,这题我们要求的是dp[i-1]=dp[nums.size()-1]-dp[i]

细节部分:

  1. 记得初始化数组,可以假设i等于0的时候,数组中有哪些是超出范围的,就对它进行初始化,或者给它扩大范围:比如说从0下标开始会越界,那么我们可以从1下标开始。但是dp数组和nums数组的映射关系就发生了变化。

  2. 从示例3中我们可以得知,如果i=0的情况下,左边的值为0

    class Solution {
    public:
    int pivotIndex(vector<int>& nums) {
    //用一维数组模板
    //记得边界情况
    int dp[10001]={0};
    dp[0]=nums[0];
    for(int i=1;i<nums.size();i++)
    {
    dp[i]=dp[i-1]+nums[i];
    }
    int n=nums.size()-1;
    for(int i=0;i<nums.size();i++)
    {
    if((i==0&&dp[n]-dp[i]==0)||(i>0&&dp[i-1]==dp[n]-dp[i]))
    {
    return i;
    }
    }
    return -1;
    }
    };

除自身以外数组的乘积(medium)

238. 除了自身以外数组的乘积 - 力扣(LeetCode)

题目解析:不能用除法,求除了本身其他数的乘积,比如i=1,那么就要i=1前面的数乘上i=1后面的数。可以考虑用前缀和思想。

解题思路:

我们可以定义两个数组,leftdp记录从0到i-1位置的全部值的乘积

rightdp记录从[i+1,n-1]的全部后缀乘积

如果我们初始化dp[0]=nums[0]的话,那么后面到算结果的时候我们依旧要挨个去维护它的边界(即i==0)的情况

所以我们可以给它扩宽一个范围,初始化dp[0]=1;

这样,我们在填写前缀和的时候,下标直接从1开始,能大胆使用i-1位置的值了。

注意:dp表与原数组nums的元素的映射关系:

从dp到nums,横坐标减1

从nums到dp,横坐标加1

复制代码
class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        long long  leftdp[100001]={1};
        long long  rightdp[100001]={1};
        int n=nums.size();
        vector<int>result(n,0);
        //从前往后
        leftdp[0]=1;
        for(int i=1;i<=nums.size();i++)
        {
            //从dp到nums,坐标减1
            leftdp[i]=leftdp[i-1]*nums[i-1];
        }
        //从后往前
        rightdp[nums.size()-1]=1;
        for(int i=nums.size()-2;i>=0;i--)
        {
            rightdp[i]=rightdp[i+1]*nums[i+1];
        }
        for(int i=0;i<nums.size();i++)
        {
            //从nums到dp,坐标+1,即leftdp[i]相当于是i左边的乘积
            result[i]=leftdp[i]*rightdp[i];
        }
        return result;
    }
};

和为 k 的子数组(medium)

560. 和为 K 的子数组 - 力扣(LeetCode)

题目解析:

在整数数组中找到一段连续区间中和为k的子数组的个数,这时候可以考虑滑动窗口+前缀和

但是这里面存在负数,那么如果示例为{-1,-1,1} k=0的时候这三个数的和都不会大于k,那么滑动窗口就不会出窗口,返回的结果就是0,但实际是有一个的,所以我们不能用滑动窗口解决该问题。

哈希表+前缀和

定义一个变量sum[i]统计[0,i]区间的变量,要想知道以i结尾有多少个连续数组等于k,就要找到从x1,x2,x3到i位置的所有元素和等于k,那么[0,x]的所有元素和不就等于sum[i]-k;

那么我们就是相当于在[0,i-1]区域内找sum[i]-k;

不需要初始化前缀和数组,我们只需要知道i位置之前有多少sum-k,sum-k标记的都是[0,x]位置的值,直接前缀和一直加即可

复制代码
class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int,int>hash;//统计前缀和出现的个数
        int sum=0,ret=0;
        hash[0]=1;//如果sum-k=0的时候可以++
        for(int i=0;i<nums.size();i++)
        {
            sum+=nums[i];//计算当前前缀和
            if(hash.count(sum-k)) ret+=hash[sum-k];//统计个数
            hash[sum]++;//不可在判断前++,当k=0的时候,ret就会把每个数都++
        }
        return ret;
    }
};

和可被 K 整除的子数组(medium)

974. 和可被 K 整除的子数组 - 力扣(LeetCode)

这题的解题思路和上一题一样

我们要求的是在以i结尾的数组中有多少个子数组可以被k整除,假设上图中a表示k的倍数,那么我们实际可以求在[0,x]中有多少(sum-a)%k等于0的数组。

补充一个知识点:同余定理

如果(a-b)%k==0那么a%k==b%k

在这题中我们找的是(sum-a)%k==0,那么就意味着sum%k==a%k,而我们求的是a%k的数组,那么这就意味这我们要在[0,i-1]中找到sum%k的数组即可

细节部分:如果sum等于负数,但是负数取余还是等于负数,这时候我们就要在取余后再加k再对它进行取余。

复制代码
class Solution {
public:
    int subarraysDivByK(vector<int>& nums, int k) {
        int sum=0,ret=0;
        unordered_map<int,int>hash;
        hash[0]=1;
        for(int i=0;i<nums.size();i++)
        {
            sum+=nums[i];
            int r=(sum%k+k)%k;
            if(hash.count(r)) ret+=hash[r];
            hash[r]++;
        }
        return ret;
    }
};

连续数组(medium)

525. 连续数组 - 力扣(LeetCode)

解题思路和和为 k 的子数组(medium)是一样的。

这题我们要求的是求含有相同数量的0和1,那么我们可以转换思维,把0全部变成-1,那么只要0和1的数量一样,最终的和都为0。

定义一个变量sum[i]统计[0,i]区间的变量,要想知道以i结尾有多少个连续数组等于0,就要找到从x到i位置的所有元素和等于k,那么[0,x]的所有元素和不就等于sum[i]-k;

那么我们就是相当于在[0,i-1]区域内找sum[i];

不需要初始化前缀和数组,

细节问题:

  • 这次的哈希表中的参数定义的是前缀和和该前缀和的下标位置,最大长度为i-hash[sum]

  • 如果有重复的sum的和的时候,我们应该要前面的下标还是后面的下标?我们要求的是最大长度,所以我们要前面的下标

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

相关推荐
重生之我是Java开发战士2 小时前
【广度优先搜索】FloodFill算法: 图像渲染,岛屿数量,岛屿的最大面积,被围绕的区域
算法·宽度优先
tankeven2 小时前
HJ147 最大 FST 距离
c++·算法
2401_857918292 小时前
分布式系统安全通信
开发语言·c++·算法
C^h2 小时前
RTthread消息队列学习
开发语言·算法·嵌入式
郝学胜-神的一滴2 小时前
冷却时间下的任务调度最优解:从原理到实现
数据结构·c++·算法·面试
sali-tec2 小时前
C# 基于OpenCv的视觉工作流-章42-模板匹配N
图像处理·人工智能·opencv·算法·计算机视觉
今儿敲了吗2 小时前
DS-2 有/无头结点的单向链表
数据结构·笔记·链表
abant23 小时前
leetcode 23合并k个有序链表
算法·leetcode·链表
啊董dong3 小时前
noi-2026年3月24号作业
数据结构·c++·算法