点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃
1.【模板】前缀和
题目链接:DP34 【模板】前缀和
题目描述:
注意这道题数组下标是从1开始的,因此我们需要申请n+1空间大小,才能到an,就是给个区间让你求这个区间的和。
算法原理:
解法一:暴力求解
题目说那个区间就去找那个区间,把区间里面的数都加起来。但是这样有个问题,时间复杂度有点大,如果每次区间都是最左端点和最右边端点,一次就是O(N),要循环q次。时间复杂度O(q*n)
解法二:前缀和
快速求出数组中某一个连续区间的和 —> 前缀和
- 预处理出来一个前缀和数组
- 使用前缀和数组
预处理出来一个前缀和数组
申请一个和原数组一样大小的数组。根据下面公式把前缀和数组填写
dp[i]表示 [1,i]区间内所有元素的和
dp[i] = dp[i-1] + arr[i]
使用前缀和数组
使用前缀和数组就是处理每次询问,如果l=3,r=5,就是让算(3,5)区间,此时不直接求,而只需要间接求出1-5所有元素的和,在再减去1-2的和,不就是3-5的和吗。因为我们数组中每个数对应的是[1,i]的和,因此dp[r]-dr[l-1]就是该区间的和。
[l,r] —> dp[r]-dp[l-1]
细节问题:为什么我们的下标要从 1 开始计数?
如果从0开始计数,当有一次[0,2]带入dp[r]-dp[l-1] ,你会发现 dp[2]-dp[-1],数组越界。
为了处理边界情况 :初始化添加虚拟节点(辅助节点)
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n,q;
cin>>n>>q;
vector<int> arr(n+1);
// 读取数据
for(int i=1;i<=n;++i) cin>>arr[i];
//1.前缀和数组
vector<long long> dp(n+1);
for(int i=1;i<=n;++i) dp[i]=dp[i-1]+arr[i];
//2.使用前缀和
int l=0,r=0;
while(q--)
{
cin>>l>>r;
cout<<dp[r]-dp[l-1]<<endl;
}
return 0;
}
2.【模板】二维前缀和
题目链接:DP35 【模板】二维前缀和
题目描述:
注意这道题下标也是从1开始的。让求以(x1,y1)为左上角,(x2,y2)为右上角的子矩阵的和。
算法原理:
解法一:暴力求解
让求那个区间,就把那个区间的和一一加起来
但是如果每次都是求最左点和最右点的子矩阵,然后循环q次。最终是时间复杂度O(nmq)
解法二:前缀和
- 预处理出来一个前缀和矩阵
- 使用前缀和矩阵
这个子矩阵难道如果自己还要去遍历一次求和吗,那时间复杂度不又高了吗。我们考虑一下把这个区间划分一下,划分成四个区域,如果求这个区间的和,我们就转换成求这个四个区域的和。把这四个区域的和加起来就好了,但是单单求每个区域也不好求,因此我们可以把区域整合一下再求。于是得到一个公式
这样就不要再一个个遍历。而是只需要一次遍历就可以把前缀和数组填写好。下面就是使用前缀和数组
这里也和上面的题一样要注意边界情况。总体时间复杂度O(nm)+O(q)
#include <iostream>
#include <vector>
using namespace std;
int main() {
// 1. 读入数据
int n,m,q;
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];
// 2. 预处理前缀和矩阵
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];
// 3. 使用前缀和矩阵
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.寻找数组的中心下标
题目链接:724. 寻找数组的中心下标
题目描述:
本题就让让找一个数组的中心下标,如果一个下标左区间的和等于该下标右区间的和,那这个下标就是中心下标。中心下标如果位于最左边认为左测之和等于0,同样右侧之和也应为0。同理中心下标如果位于最右侧认为右侧之和等于0,那左侧之和也应为0。如果不存在中心下标返回-1。如果有多个中心下标返回最左侧的。
算法原理:
解法一:暴力求解
每次给一个i下标,然后把它左侧 0~i-1 和 与 右侧 i+1~n-1和 求出来,然后在判断。但是这样是复杂度是O(N*N)
因为想判断i这个位置是否是中心下标,我仅需要判断一下 0~i-1 的和,那使用前缀和数组不就正好可以解决某一个连续数据的和吗。同理求 i+1~n-1区间的和,可以把前缀和稍微改一下,改成后缀和。前缀和是从左往后走,那后缀和是倒着走从右往左走!
解法二:前缀和
虽然我们前面有前缀和的模板,但是不要死记模板,一定要充分理解思想。然后碰到不同的题,才会不怕题目的特殊要求。前面模板是
dp[i]=dp[i-1]+nums[i],这是它数组下标从1开始,而我们这道题数据下标从0开始,需要特殊处理一下!
因为我们这里需要两个数组,所以就定义一个前缀和数组,一个后缀和数组。
因为求的是i是否是中心下标,所以对于前缀和数组 f[i] 我仅需要 0~ i-1 这段区间所有元素的和,对于后缀和数组 g[i] 仅需要 i+1 ~ n-1 这段区间所有的和。这和前面是不一样的的
如何根据这个分析,得到递推公式呢?就是把状态标识转化成状态转移方程。
f[i] 表示的是 0 ~ i-1 所有元素的和,那 f[i-1] 不就是 0 ~ i-2 所有元素的和吗,此时在加 上nums[i-1] ,不就是 0~ i-1 所有元素的和吗
g[i] 表示 i+1 ~ n-1 所有元素的和,g[i+1]不就表示 i+2 ~ n-1所有元素的和吗,此时在加上nums[i+1],不就是 i+1 ~ n-1 所有元素的和吗
前缀和数组+后缀和数组预处理好了,接下来就是使用前缀和数组+后缀和数组了
但是这里有细节问题,前面的题数组下标是从1开始的。但是今天这道题是从数组0下标开始的,不特殊处理代入公式会有越界风险。并且有一个前缀和数组还有一个后缀和数组,那后缀和数组从哪里开始算呢?
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int n=nums.size();
vector<int> f(n),g(n);
//处理细节问题,但是初始时本来数组都是0,因此可以不用在写了
// f[0]=0;
// g[n-1]=0;
//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];
//2.使用前缀和数组+后缀和数组
for(int i=0;i<n;++i)
if(f[i]==g[i])
return i;
return -1;
}
};
4.除自身以外数组的乘积
题目链接:238. 除自身以外数组的乘积
题目描述:
算法原理:
解法一:暴力求解
给一个位置就求除它之外所有位置的乘积。时间复杂度O(N^2)。
处理一个问题直接解决不太好解决,先预处理一个数组。----> 前缀和思想
,典型的以空间换时间
解法二:前缀和
记住前缀和是一个思想,千万不要死记模板,记得应该是思想!
然后根据状态标识得到一个状态转移方式
要求 0 ~ i-1 区间所有元素的积,说明已经知道 0 ~ i-2 区间所有元素的积,
0 ~ i-2 区间所有元素的积在乘一个nums[i-1] 不就是 0 ~ i-1 区间所有元素的积了嘛,而0 ~ i-2 区间所有元素的积是 f[i-1] ,所以前缀积方式就有了。同理后缀也是一样的。
现在预处理好了,接下来就是使用的问题了
直接申请一个和原数组大小,数组中填的是 f[i]*g[i]
这里依然有细节问题,注意越界问题,并且不能给0,不然乘积全都是0。
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n=nums.size();
vector<int> f(n),g(n),ret(n);
// 处理细节
f[0]=g[n-1]=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];
// 2.使用
for(int i=0;i<n;++i)
ret[i]=f[i]*g[i];
return ret;
}
};