文章目录
- 上期回顾
- 前缀和
-
- 算法简介
-
- [1. 什么是前缀和?](#1. 什么是前缀和?)
- [2. 什么情况下使用前缀和?](#2. 什么情况下使用前缀和?)
- [3. 前缀和的常见题型](#3. 前缀和的常见题型)
- 模板题
- 算法题

◆ 博主名称:此生决int
大家好,欢迎来到我的博客~
⭐ 个人专栏:快速复习系列
⭐ 热门专栏:算法基础到精通系列
上期回顾
上期我们主要学习了 滑动窗口!!
文章概要
本文将从头带你学会前缀和这一经典算法,从易到难,层层递进,本文先带你熟悉前缀和的模版,再通过6道经典的前缀和算法题帮你快速搞懂并拿下他!!
前缀和
算法简介
1. 什么是前缀和?
前缀和是一种常见的预处理算法。
它通过提前统计数组前 (i) 个元素的总和,并保存到 sum[i] 中,从而快速计算任意区间的元素和。
核心思想:
sum[i]表示原数组前i个元素的和- 区间
[l,r]的和可以快速表示为:
tex
sum[r]-sum[l-1]
这样就能将原本需要 (O(n)) 的区间求和,优化到 (O(1))。
2. 什么情况下使用前缀和?
当前题目需要:
- 多次查询区间和
- 频繁统计某一段区间的信息
- 优化暴力枚举的区间计算
通常都可以考虑使用前缀和进行优化。
3. 前缀和的常见题型
-
一维前缀和
用于数组中的区间求和问题。
-
二维前缀和
用于矩阵中的子矩阵求和问题。
模板题
一维前缀和
题目链接:
解题代码:
cpp
//记得用 long long,防止前缀和爆 int
//题目下标从 1 开始,所以前缀和不用额外偏移处理
#include <iostream>
using namespace std;
const int N=1e5+10;
//arr[i] 表示前 i 个数的前缀和(包括第 i 个数)
long long arr[N];
int main() {
int n,m;
cin>>n>>m;
//读入数组并预处理前缀和
for(int i=1;i<=n;i++)
{
long long x;
cin>>x;
//当前前缀和 = 前一个前缀和 + 当前值
arr[i]=arr[i-1]+x;
}
//进行 m 次区间查询
while(m--)
{
int l,r;
cin>>l>>r;
//[l,r] 区间和 = 前 r 项和 - 前 l-1 项和
cout<<arr[r]-arr[l-1]<<endl;
}
}
//64 位输出请用 printf("%lld")
二维前缀和
题目链接:
解题代码:
cpp
//dp[i][j] 表示从 (1,1) 到 (i,j) 这个矩形区域的元素和
//递推公式:dp[i][j]=dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+arr[i][j]
#include <iostream>
using namespace std;
const int N=1e3+10;
//二维前缀和数组
long long dp[N][N];
int main() {
int n,m,q;
cin>>n>>m>>q;
//读入矩阵并预处理二维前缀和
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
int x;
cin>>x;
//左边 + 上边 - 重复部分 + 当前值
dp[i][j]=dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+x;
}
}
//进行 q 次子矩阵查询
while(q--)
{
int x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
//(x1,y1) 到 (x2,y2) 的子矩阵和
cout<<dp[x2][y2]-dp[x2][y1-1]-dp[x1-1][y2]+dp[x1-1][y1-1]<<endl;
}
}
//64 位输出请用 printf("%lld")
算法题
1,寻找数组的中心下标
题目链接:
解题思路
先计算整个数组的元素总和,然后遍历数组,用 left 维护当前位置左边元素和,则右边元素和可通过 sum-left-nums[i] 得到。若左右两边元素和相等,则当前位置就是中心下标。
解题代码
cpp
class Solution {
//前缀和和后缀和数组
//注意小标要从1开始
//f[n]g[n]表示的含义也和之前的有点不一样
//这里表示f[n]前面的数的和,不包括自己
public:
int pivotIndex(vector<int>& nums) {
int n=nums.size();
//f[i] 表示 i 左边元素和(不包括自己)
vector<int>f(n);
//g[i] 表示 i 右边元素和(不包括自己)
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;
}
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution {
public:
int pivotIndex(vector<int>& nums) {
//先求数组总和
int sum=0;
for(auto x:nums)
sum+=x;
//left 表示当前位置左边元素和
int left=0;
for(int i=0;i<nums.size();i++)
{
//右边元素和 = 总和 - 左边和 - 当前值
int right=sum-left-nums[i];
//找到中心下标
if(left==right)
return i;
//更新左边元素和
left+=nums[i];
}
return -1;
}
};
2,除自身以外数组的乘积
题目链接:
解题思路
由于题目要求不能使用除法,因此我们可以分别预处理每个位置左边元素的乘积和右边元素的乘积。最后当前位置答案就是左边乘积 × 右边乘积。
解题代码
cpp
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
//不能使用除法
//f[i] 表示 i 左边元素乘积
//g[i] 表示 i 右边元素乘积
int n=nums.size();
vector<int>f(n);
vector<int>g(n);
//边界初始化
f[0]=1;
g[n-1]=1;
//预处理前缀乘积
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];
vector<int>answer;
//当前位置答案 = 左边乘积 × 右边乘积
for(int i=0;i<n;i++)
answer.push_back(f[i]*g[i]);
return answer;
}
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n=nums.size();
//answer[i] 先存左边乘积
vector<int>answer(n,1);
//预处理前缀乘积
for(int i=1;i<n;i++)
answer[i]=answer[i-1]*nums[i-1];
//right 表示当前位置右边乘积
int right=1;
//边遍历边乘上后缀乘积
for(int i=n-1;i>=0;i--)
{
answer[i]*=right;
right*=nums[i];
}
return answer;
}
};
3,和为 K 的子数组
题目链接:
解题思路
由于数组中可能存在负数,因此双指针不具有单调性,无法使用。我们利用前缀和统计区间和,再结合哈希表快速查找是否存在 f[i]-k,从而统计和为 k 的子数组个数。
解题代码
cpp
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
//双指针不行
//因为 nums[i] 可能为负数,不具有单调性
//f[i] 表示前 i 个元素的和
//即下标 < i 的元素和
int n=nums.size();
int count=0;
//前缀和数组
vector<int>f(n+1);
//mp[x] 表示前面前缀和 x 出现的次数
unordered_map<int,int>mp;
//前缀和为 0 初始化为 1 次
mp[0]=1;
for(int i=1;i<=n;i++)
{
//计算前缀和
f[i]=f[i-1]+nums[i-1];
//寻找前面是否存在 f[i]-k
count+=mp[f[i]-k];
//加入当前前缀和
//必须先统计再加入,否则 k=0 会重复计算
mp[f[i]]++;
}
return count;
}
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
//sum 表示当前前缀和
int sum=0;
//统计答案
int count=0;
//mp[x] 表示前缀和 x 出现次数
unordered_map<int,int>mp;
//空前缀初始化
mp[0]=1;
for(auto x:nums)
{
//更新前缀和
sum+=x;
//查找是否存在 sum-k
count+=mp[sum-k];
//记录当前前缀和
mp[sum]++;
}
return count;
}
};
4,和可被 K 整除的子数组
题目链接:
解题思路
利用同余定理,如果两个前缀和对 k 取模后的结果相同,那么它们之间的子数组和一定能被 k 整除。再结合哈希表统计每个余数出现的次数即可。
解题代码
cpp
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k) {
//同余定理
//如果两个前缀和 %k 的结果相同
//那么它们之间的子数组和一定 %k==0
int n=nums.size();
int count=0;
//前缀和数组
vector<int>f(n+1);
//mp[x] 表示余数 x 出现的次数
unordered_map<int,int>mp;
//前缀和为 0 的余数初始化
mp[0]=1;
for(int i=1;i<=n;i++)
{
//计算前缀和
f[i]=f[i-1]+nums[i-1];
//注意负数取模情况
int mod=(f[i]%k+k)%k;
//统计相同余数个数
count+=mp[mod];
//记录当前余数
mp[mod]++;
}
return count;
}
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k) {
//sum 表示当前前缀和
int sum=0;
//统计答案
int count=0;
//mp[x] 表示余数 x 出现次数
unordered_map<int,int>mp;
//空前缀初始化
mp[0]=1;
for(auto x:nums)
{
//更新前缀和
sum+=x;
//统一处理负数取模
int mod=(sum%k+k)%k;
//统计相同余数
count+=mp[mod];
//记录当前余数
mp[mod]++;
}
return count;
}
};
5,连续数组
题目链接:
解题思路
把数组中的 0 看成 -1,这样题目就转化成:寻找和为 0 的最长连续子数组。利用前缀和 + 哈希表,如果两个位置前缀和相同,说明它们之间的区间和为 0。
解题代码
cpp
class Solution {
//把 0 看成 -1
public:
int findMaxLength(vector<int>& nums) {
//双指针不行
//因为不具有单调性
//把 0 变成 -1 后
//题目转化成最长和为 0 的子数组
int n=nums.size();
//f[i] 表示前 i 个元素的和
vector<int>f(n+1);
//mp[x] 表示前缀和 x 第一次出现的位置
unordered_map<int,int>mp;
//前缀和 0 在下标 0 出现
mp[0]=0;
int ret=0;
for(int i=1;i<=n;i++)
{
//0 看成 -1
if(nums[i-1]==0)
f[i]=f[i-1]-1;
else
f[i]=f[i-1]+1;
//如果前缀和出现过
//说明中间区间和为 0
if(mp.count(f[i]))
ret=max(ret,i-mp[f[i]]);
else
//只记录第一次出现位置
mp[f[i]]=i;
}
return ret;
}
};
//i 0 1
//nums 0 1
//f[i] 0 -1 0
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution {
public:
int findMaxLength(vector<int>& nums) {
//sum 表示当前前缀和
int sum=0;
//记录答案
int ret=0;
//mp[x] 表示前缀和 x 第一次出现位置
unordered_map<int,int>mp;
//空前缀初始化
mp[0]=-1;
for(int i=0;i<nums.size();i++)
{
//0 看成 -1
if(nums[i]==0)
sum--;
else
sum++;
//前缀和相同
//说明中间区间和为 0
if(mp.count(sum))
ret=max(ret,i-mp[sum]);
else
//只保留第一次出现位置
mp[sum]=i;
}
return ret;
}
};
6,矩阵区域和
题目链接:
解题思路
这题是二维前缀和模板题。先预处理二维前缀和数组,然后枚举每个位置,计算以当前位置为中心、边长由 k 决定的矩形区域和即可。
解题代码
cpp
class Solution {
//计算以 (i,j) 为中心的矩形区域和
int blocksum(int i,int j,int k,vector<vector<int>>&f)
{
//确定左上角和右下角边界
int x1=i+1-k>0?i+1-k:1,
x2=i+1+k<f.size()?i+1+k:f.size()-1,
y1=j+1-k>0?j+1-k:1,
y2=j+1+k<f[0].size()?j+1+k:f[0].size()-1;
//二维前缀和求子矩阵和
return f[x2][y2]-f[x1-1][y2]-f[x2][y1-1]+f[x1-1][y1-1];
}
public:
vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
//二维前缀和模板题
//注意下标从 1 开始
int n=mat.size(),m=mat[0].size();
//二维前缀和数组
vector<vector<int>>f(n+1,vector<int>(m+1));
//预处理二维前缀和
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
f[i][j]=f[i-1][j]+f[i][j-1]-f[i-1][j-1]+mat[i-1][j-1];
}
}
//答案数组
vector<vector<int>>ret(n,vector<int>(m));
//枚举每个位置
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
ret[i][j]=blocksum(i,j,k,f);
}
}
return ret;
}
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution {
public:
vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
int n=mat.size(),m=mat[0].size();
//二维前缀和
vector<vector<int>>sum(n+1,vector<int>(m+1));
//预处理二维前缀和
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+mat[i-1][j-1];
//答案数组
vector<vector<int>>ret(n,vector<int>(m));
//直接计算每个矩形区域和
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
//确定矩形边界
int x1=max(0,i-k),y1=max(0,j-k);
int x2=min(n-1,i+k),y2=min(m-1,j+k);
//转成前缀和下标
x1++,y1++,x2++,y2++;
//二维前缀和求子矩阵和
ret[i][j]=sum[x2][y2]-sum[x1-1][y2]-sum[x2][y1-1]+sum[x1-1][y1-1];
}
}
return ret;
}
};
下期预告
今天的分享就到这里了,下一期我们将一起学习位运算相关的算法知识!!期待你的关注!
结语
本期内容就到这里啦,欢迎大家在评论区一起交流讨论
如果你也在为蓝桥杯/ACM备赛头疼,或是准备算法面试找不到系统学习路径,欢迎订阅我的「算法从入门到精通」专栏!
这里没有枯燥的理论堆砌,只有完整的算法学习路线 ,
搭配精选梯度习题+清晰思路解析,帮你把每个算法学透、练熟。包教包会的!
我们一起在算法路上稳步进阶!

