文章目录
- 实现原理
- 实现思路
- 一维前缀和模板
- 二维前缀和模板
- 典型例题
- 一维前缀和
- 二维前缀和
- 寻找数组中心下标
- 除自身以外数组的乘积
- 关系矩阵和
- 总结
实现原理
前缀和问题和二分查找类似,也是有一些固定的模板的,在理解原理的基础上进行实践,就能解决大多数问题
前缀和问题的题目问法通常是计算某个序列中子序列的之和,如果采用暴力求解的话就是遍历一遍数组,假设要找M
个子序列,序列长度总共为N
,那么时间复杂度就是O(M*N)
,而前缀和算法原理可以优化这个时间复杂度
实现原理有些类似于动态规划,原理就是预处理一个前缀和数组,然后利用前缀和数组解决问题即可
下面介绍算法的实现思路
实现思路
现在我们有数组arr,要求的结果是取出q
和arr
中的子序列,计算这些子序列的长度,算法原理开辟一个和arr
一样大的数组,作为dp
数组,数组内存储的是对应arr
中对应下标前所有位置数之和
一维前缀和模板
vector<long long> v(n+1);
for (int i = 1; i <= n; i++)
{cin>>v[i];
}
vector<long long> dp(n+1);
dp[0]=0;
for (int i = 1; i <= n; i++)
{dp[i]= dp[i - 1] + v[i];
}
自此,一维前缀和总体结束,下面是二维前缀和的题目解析
整体来看,二维前缀和和一维前缀和相差不算很大,代码实现的思路也很好想,可以图解如下表示
假设现在有下图所示的二维序列,这里面存储的是一些数据
依据题意,假设这里需要求4,3
的内容,因此需要计算的是这片区域的面积
因此,我们可以仿照前面的算法,也用dp
原理创建一个dp
数组,数组中每一个元素存储的是从1,1
到该位置这块区域内的所有元素的和
那么区间内数组如何计算?如果采用一个一个遍历找的时间复杂度显然是不合适的,因此需要找到一种更高效的复杂度方式,可以采用如下方式
对于我们要求的彩色区域,可以划分为四个子区域进行求解,那么面积如何求?如果采用A+B+C+D
直接求,是不合理的,原因在于B和C
的面积不能直观的求解,因此可以改成(A+C)+(A+B)+D-A
,通过这样划分区域,就可以求解问题
抽象化上面的思路,假设要求i,j位置的值,就可以抽象化为dp[i][j]=dp[i-1][j]+dp[i][j-1]+arr[i][j]-dp[i-1][j-1]
那现在根据上面的算法原理,已经把dp
数组求出来了,那么后续如何求解?
假设现在要求的是2,2,4,4
,理论上所求区域如下
那么对应到dp数组
就是下面的区域
将整个区域进行抽象化演示,即看成从m,n
到p,q
因此这个区域面积就可以表示为dp[m][n]-dp[p-1][q]-dp[p][q-1]+dp[p-1][q-1]
因此,二维前缀和的做题思路就抽象化出来了,首先用dp数组表示出一个和数组,再利用和数组面积的加减关系求解出最终答案即可
二维前缀和模板
// 数据输入
for (int i = 1; i <= m; i++)
{for (int j = 1; j <= n; j++){cin >> v[i][j];}
}
// 处理dp数组
vector<vector<long long>> dp(m + 1, vector<long long>(n + 1));
for (int i = 1; i <= m; i++)
{for (int j = 1; j <= n; j++){dp[i][j] = dp[i - 1][j] + dp[i][j - 1] + v[i][j] - dp[i - 1][j - 1];}
}
典型例题
一维前缀和
#include <bits/stdc++.h>
using namespace std;int main()
{int n, q;cin >> n >> q;vector<long long> v(n+1);for (int i = 1; i <= n; i++){cin>>v[i];}vector<long long> dp(n+1);dp[0]=0;for (int i = 1; i <= n; i++){dp[i]= dp[i - 1] + v[i];}while (q--){int l, r;cin >> l >> r;cout << dp[r] - dp[l - 1] << endl;}return 0;
}
二维前缀和
#include <iostream>
#include <vector>
using namespace std;int main()
{int m = 3, n = 4, q = 3;cin >> m >> n >> q;vector<vector<int>> v(m + 1,vector<int>(n+1));// 数据输入for (int i = 1; i <= m; i++){for (int j = 1; j <= n; j++){cin >> v[i][j];}}// 处理dp数组vector<vector<long long>> dp(m + 1, vector<long long>(n + 1));for (int i = 1; i <= m; i++){for (int j = 1; j <= n; j++){dp[i][j] = dp[i - 1][j] + dp[i][j - 1] + v[i][j] - dp[i - 1][j - 1];}}// 处理数据while (q--){int x1, y1, x2, y2;cin >> x1 >> y1 >> x2 >> y2;cout << dp[x2][y2] - dp[x2][y1-1] - dp[x1-1][y2] + dp[x1 - 1][y1 - 1] << endl;}return 0;
}
寻找数组中心下标
本题应用了前缀和的思路,写入两个表,一个从后向前写,一个从前向后写,其中要注意对于这个题的边界下标问题,尤其是从右向左写容易越界
处理前缀和问题要想清楚,前后缀和数组的含义是当前下标之前/后元素,不含当前下标元素
class Solution
{
public:int pivotIndex(vector<int>& nums) {int n=nums.size();// 处理前缀和vector<int> f(n);f[0]=0;for(int i=1;i<n;i++){f[i]=f[i-1]+nums[i-1];}// 处理后缀和vector<int> g(n);g[n-1]=0;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;}
};
除自身以外数组的乘积
此题也是很明显的前缀和问题,在解题的时候,要理清思路,一要处理好边界问题,二要想清楚原理,边界问题提前处理
class Solution
{
public:vector<int> productExceptSelf(vector<int>& nums){int n = nums.size();// 前缀和数组vector<int> f(n);f[0] = 1;for (int i = 1; i < n; i++){f[i] = f[i - 1] * nums[i - 1];}// 后缀和数组vector<int> g(n);g[n - 1] = 1;for (int i = n - 2; i >= 0; i--){g[i] = g[i + 1] * nums[i + 1];}// 返回数组vector<int> res(n);res[0] = g[0];res[n - 1] = f[n - 1];for (int i = 1; i < n - 1; i++){res[i] = f[i] * g[i];}return res;}
};
关系矩阵和
二维数组的前缀和问题中的简单应用,其中关于二维数组前缀和主要就是要注意dp
数组的写入,在这个过程中涉及到的边界问题,其余如果理解原理应用模板是可以很好解决的
class Solution
{
public:vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {int m=mat.size();int n=mat[0].size();// 创建前缀和二维数组vector<vector<int>> dp(m+1,vector<int>(n+1));for(int i=1;i<=m;i++){for(int j=1;j<=n;j++){dp[i][j]=dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+mat[i-1][j-1];}}// 算数据vector<vector<int>> ans(m,vector<int>(n));for(int i=0;i<m;i++){for(int j=0;j<n;j++){int x1=max(0,i-k)+1,y1=max(0,j-k)+1;int x2=min(m,i+k+1),y2=min(n,j+k+1);ans[i][j]=dp[x2][y2]-dp[x2][y1-1]-dp[x1-1][y2]+dp[x1-1][y1-1];}}return ans;}
};
总结
前缀和问题也是较为重要的算法,重点就是要理解dp
数组的意义,尤其是对于二维数组的前缀和,要理解才能正确写出