简介
核心思想是:提前计算出从起点到每个位置的累加和,这样当需要求某个区间的和时,不需要遍历区间内的每个元素,直接用两个前缀和相减即可得到,时间复杂度从 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)

题目解析:找到一个下标,使它左边的和等于右边的和。
这里可以用到前缀和,可以套用一维数组的模板,先求下标0开始到i下标的所有和(dp[i]=dp[i-1]+nums[i]),根据题意,这题我们要求的是dp[i-1]=dp[nums.size()-1]-dp[i]
细节部分:
-
记得初始化数组,可以假设i等于0的时候,数组中有哪些是超出范围的,就对它进行初始化,或者给它扩大范围:比如说从0下标开始会越界,那么我们可以从1下标开始。但是dp数组和nums数组的映射关系就发生了变化。
-
从示例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)

题目解析:
在整数数组中找到一段连续区间中和为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)

解题思路和和为 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;
}
};