【模板】前缀和_牛客题霸_牛客网
该算法是先预处理一个数组,用空间换时间,将原本时间复杂度为O(n2)降为O(n)
题中下标(用i表示)从1开始计数,长度为n的数组,想访问到an 位置,创建数组时要创建大小为n+1的数组
解法一:暴力解法——模拟:题中q次询问,每次询问按要求从头遍历即可,时间复杂度为O(n*q),根据题中的数据范围,大概为O(1010)
解法二:前缀和——快速求出数组某一连续区间的和(快速时间复杂度O(1),相比解法一遍历O(n)快许多),整体时间复杂度O(q)+O(n)。这个O(n)是我们在预处理前缀和数组时需要遍历一遍原数组。
因为[1,r]这段区间的和与[1,l]这段区间的和本质上是同一类问题,当我们研究同一类问题时,我们可以把这些同一类问题抽象成状态表示,进而用动态规划的思想解决。
如果我们要访问[0,2]区间的数组时,根据上面总结的我们需要访问dp[2]和dp[-1]这两段区间的数组和,但是-1这个位置访问不到,此时需要处理边界情况。但我们从下标1开始计数不会有问题(dp[0]置为0即可,不影响前缀和)
#include
#include
using namespace std;
int main()
{
//1.读取数据
int n,q;
cin >> n >> q;
vector<int> arr(n+1);
for(int i = 1;i <= n;i++) cin >> arr[i];
//2.预处理前缀和数组
vector<long long> dp(n+1); //防止溢出
for(int i=1;i<=n;i++) dp[i] = dp[i-1]+arr[i];
//3.使用前缀和数组
int l=0,r=0;
while(q--)
{
cin >> l >> r;
cout << dp[r] - dp[l-1] << endl;
}
return 0;
}
【模板】二维前缀和_牛客题霸_牛客网
解法一:暴力解法——模拟
时间复杂度O(mnq)
解法二:前缀和(时间复杂度O(m*n)+O(q))
使用前缀和矩阵(每一次求消耗时间复杂度O(1),一共q次即为O(q))
题中要求求出[x1][y1]到[x2][y2]区间和时,此时我们还可以将他划分为四个部分,即区间D为题中要求。我们可以先把图中整个和求出来(A+B+C+D)然后减去其他部分面积。求得公式后,我们接下来求区间就可以用O(1)的时间复杂度求
这样,我们填写前缀和矩阵数组的时候,下标直接从 1 开始,能⼤胆使⽤ i - 1 , j - 1 位置的值。
注意: dp 表与原数组内的元素的映射关系:
i. 从 dp 表到原矩阵,横纵坐标减⼀;
ii. 从原矩阵到 dp 表,横纵坐标加⼀
#include
using namespace std;
const int N = 1010;
int arr[N][N];
long long dp[N][N];
int n, m, q;
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 - 1][j] + dp[i][j - 1] + arr[i][j] - dp[i - 1][j -
1];
// 使⽤前缀和矩阵
int x1, y1, x2, y2;
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;
}
寻找数组的中心下标
class Solution {
public:
int pivotIndex(vector<int>& nums)
{
int n=nums.size();
vector<int> f(n),g(n);
//1.预处理前缀数组和后缀数组
for(int i=1;i<n;i++)
f[i] = f[i-1] + nums[i-1];
for(int i=n-2;i>=0;i--) //倒着填,最后一个位置是n-1,但是g[n-1]位置上的值是0就可以,n-1会越界
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;
}
};
除自身以外数组的乘积
g表示后缀积,g[i]表示i+1n-1这段区间的积,同理想求这段区间的乘积时,我们已经直到了i+2n-1这段区间的乘积(即g[i+1]已知)则g[i] = g[i+1]*nums[i+1]
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums)
{
int n=nums.size();
vector<int> f(n),g(n);
//1.预处理前缀数组和后缀数组
f[0] = 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--) //倒着填,最后一个位置是n-1,但是g[n-1]位置上的值是1就可以,n-1会越界
g[i] = g[i+1]*nums[i+1];
//2.使用
vector<int>ret(n);
for(int i=0;i<n;i++)
ret[i] = f[i]*g[i];
return ret;
}
};
和为k的子数组
注意:此题不能用滑动窗口的方法解决,因为当定义left和right进行移动时,由于数组中有正有负的情况,可能存在前面有段区间的负数和与后面区间正数和相抵消的情况,但以为此时left和right都向右移动,就会漏掉这种情况,不符合单调性这一性质。
class Solution {
public:
int subarraySum(vector<int>& nums, int k)
{
unordered_map<int, int> hash; // 统计前缀和出现的次数
hash[0] = 1;
int sum = 0, ret = 0;
for(auto x : nums)
{
sum += x; // 计算当前位置的前缀和
if(hash.count(sum - k)) ret += hash[sum - k]; // 统计个数
hash[sum]++;
}
return ret;
}
};
和可被K整除的⼦数组
返回其中元素之和可被 k 整除的(连续、非空) 子数组 的数目。
暴力枚举:枚举出所有子数组,求和判断是否能被k整除。
**前缀和+hash:**找出两个前缀和一个以i为结尾的前缀和sum;另一个标记为x。根据题目要求,我们有(sum-x)%k=0,根据同余定理可得,sum%k等于x%k。所以此时问题转换为只需要在[0,i-1]区间找有多少个前缀和余数等于sum%k. 同时根据C++负数%正数,我们需要将其修正为(sum%k+k)%k。并且该题并不需要真的创建一个前缀和数组,因为我们只需要记录前缀和的余数即可。此时我们创建一个hash
补充知识:
注:该题依然不能用滑动窗口思想来解题,因为有可能出现负数或者0的情况。
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k)
{
unordered_map<int, int> hash;
hash[0 % k] = 1; // 0 这个数的余数
int sum = 0, ret = 0;
for(auto x : nums)
{
sum += x; // 算出当前位置的前缀和
int r = (sum % k + k) % k; // 修正后的余数
if(hash.count(r)) ret += hash[r]; // 统计结果
hash[r]++;
}
return ret;
}
};
连续数组
找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。
如果我们直接去统计0和1出现的个数,这样难度会有点大,不妨我们转换一下思路,我们把0全部变成-1,那就转化为在数组中找出最长的子数组,找出和为0即可。我们之前做过一道和为k的子数组,这样就容易一点。
前缀和+hash:这里思路和和为k的子数组一样,这里主要考虑一些细节问题:
class Solution {
public:
int findMaxLength(vector<int>& nums)
{
unordered_map<int, int> hash;
hash[0] = -1; // 默认有⼀个前缀和为 0 的情况
int sum = 0, ret = 0;
for(int i = 0; i < nums.size(); i++)
{
sum += nums[i] == 0 ? -1 : 1; // 计算当前位置的前缀和
if(hash.count(sum)) ret = max(ret, i - hash[sum]);
else hash[sum] = i;
}
return ret;
}
};
矩阵区域和
answer矩阵中每一个位置返回的值是原矩阵中以该位置为中心,上下左右同时扩展k个格子,所组成的矩阵的和,填入answer,如果超出矩阵范围不计算,此时answer[0][0]位置为12
本质是快速求出矩阵某一范围的和,用二维前缀和。
注意: dp 表与原数组内的元素的映射关系:
i. 从 dp 表到原矩阵,横纵坐标减⼀;
ii. 从原矩阵到 dp 表,横纵坐标加⼀
在第一步求坐标的时候直接+1,然后直接拿值即可
class Solution {
public:
vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k)
{
int m = mat.size(), n = mat[0].size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
// 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];
// 2. 使⽤
vector<vector<int>> ret(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 - 1, i + k) + 1, y2 = min(n - 1, j + k) + 1;
ret[i][j] = dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] +dp[x1 - 1][y1 - 1];
}
return ret;
}
};