目录
牛客/leetcode题目
一、【模板】一维前缀和
1.第一行输入n和q, 表示数组元素个数和q次询问
2.第二行输入数组的n个元素
3.剩下的q行每行输入两个数,表示起始位置和终止位置
4.求数组从起始位置和终止位置这段区间的和
注意:数组的下标从1开始
2.算法分析
解法一:暴力解法
简单模拟题目要求, 每次询问把要求区间的元素遍历求和
时间复杂度: O(q*n)
解法二:前缀和
前缀和用于解决快速求数组中某一个连续区间的和的问题
时间复杂度: O(q)
前缀和算法步骤:
1.预处理出来一个前缀和数组 dp (和原始数组大小一样)
dp[i]表示 arr 数组 [1, i] 区间内所有元素的和
结论: dp[i] = dp[i-1] + arr[i]
2.使用前缀和数组
如果题目要求的是 arr数组 [l, r] 的和, 那么我们可以间接求,用[1, r]区间的和 减去 [1, l-1]区间的和求实[l, r]区间的和, 而[1, r]区间的和是dp[r], [1, l-1]的和是 dp[l-1], 而dp数组我们已经预处理好了,因此求和只需要O(1)的时间复杂度
而如果是q次询问,最终时间复杂度是 O(n)+O(q)
细节问题:为啥数组的下标最好从1开始?
·如果从0开始计数,要求的是[0, 2]区间的和, 那么 结果就是 dp[2] - dp[-1], 发生了越界访问, 需要特殊处理一下边界情况!
·如果从1开始计数,求的是[1, 2]区间的和, 那么 结果就是 dp[2] - dp[0], 在预处理数组时只需要让dp[0] = 0 即可, 求出来的就是dp[2],也就是[1, 2]区间的和
3.算法代码
#include <iostream>
using namespace std;
#include<vector>
int main()
{
//1.读入数据
int n, q;
cin >> n >> q;
vector<int> arr(n+1, 0);
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;
}
二、【模板】二维前缀和
给定一个n行m列的矩阵,q次询问,每次询问输入4参数,代表(x1, y1), (x2, y2), 输出以(x1, y1)为左上角,以(x1, y1)为右下角的子矩阵的和
2.算法分析
解法一:暴力解法
每次询问,老老实实把要求的矩阵和遍历一遍求和即可 --- O(n*m*q)
解法二:前缀和解法
1.预处理出来一个前缀和矩阵 dp (和原始矩阵大小一样)
dp[i][j]表示从 arr矩阵中从 [1, 1]到 [i, j] 这个子矩阵的所有元素的和
求解dp矩阵:
2.使用前缀和矩阵
总的时间复杂度: O(m*n) + O(q)
细节问题:为啥下标从[1, 1]开始,原因和一维前缀和模板中的理由一样,此处就不赘述了~
3.算法代码
#include <iostream>
using namespace std;
#include <vector>
int main()
{
//1.读入数据
int n = 0, m = 0, q = 0;
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;
}
三、寻找数组的中心下标
724. 寻找数组的中心下标 - 力扣(LeetCode)https://leetcode.cn/problems/find-pivot-index/1.题目解析
1.求数组中心下标
2.中心下标是指该下标的左侧元素之和等于该下标的右侧元素之和
3.中心下标在左端,左侧元素和是0, 中心下标在最右边,右侧元素和是0
4.有多个中心下标,返回最左边的一个
2.算法分析
解法一: 暴力解法
每次枚举一个下标,求出该下标左侧元素之和和右侧元素之和 --- O(N^2)
解法二: 前缀和思想
f表示前缀和数组,g表示后缀和数组
f[i] 表示 [0, i-1] 区间,所有元素的和 ---》 f[i] = f[i-1] + nums[i-1]
g[i] 表示 [i+1, n-1] 区间,所有元素的和 ---》 g[i] = g[i+1] + nums[i+1]
注意: 前缀和(后缀和)数组的表达式就提论题,此题要求的是i之前的所有元素之和 与 i之后的所有元素之和,因此不包含nums[i], 所以f[i]和g[i]的含义较上道题目有所变化
因此判断 i 是否是中心下标只需要看 f[i] 是否 == g[i] 即可
细节处理:
1. f[0]=0, g[n-1]=0
2. f表从左向右填,g表从右向左填
3.算法代码
class Solution {
public:
int pivotIndex(vector<int>& nums)
{
int n = nums.size();
//1.预处理前缀和、后缀和数组
vector<int> f(n); //vector默认值就是0
vector<int> g(n); //vector默认值就是0
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;
}
};
四、除自身以外数组的乘积
238. 除自身以外数组的乘积 - 力扣(LeetCode)https://leetcode.cn/problems/product-of-array-except-self/1.题目解析
求每个元素除自身之外其余所有元素的乘积
2.算法分析
前缀积 f[i] 表示[0, i-1] 区间内所有元素的乘积 f[i] = f[i-1] * nums[i-1]
后缀积 g[i] 表示[i+1, n-1]区间内所有元素的乘积 g[i] = g[i+1] * nums[i+1]
细节问题:
1. f[0]=1, g[n-1]=1
2. f表从左向右填,g表从右向左填
3.算法代码
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums)
{
int n = nums.size();
vector<int> f(n);
vector<int> 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--)
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的子数组
560. 和为 K 的子数组 - 力扣(LeetCode)https://leetcode.cn/problems/subarray-sum-equals-k/1.题目解析
数组中元素可正可负可为零, 返回和为k的子数组(连续)的个数
2.算法分析
暴力解法:
每次从前向后固定一个下标,然后向后枚举,找到了和为k的子数组,不能停,继续向后枚举,因为数组中元素可正可负可为零,因此每次固定一个下标后从该下标枚举到数组结尾!
不能用滑动窗口算法的原因:
如图所示,假如现在[left, right]区间所有元素之和等于k, 而黄色部分和蓝色部分元素之和恰好等于0, 则中间白色部分元素之和也就等于k, 因此如果right不回退,就会错过一些正确答案,因此不能使用滑动窗口算法~
前缀和算法:
我们只需要遍历原数组一遍,每次统计出 i 位置为结尾的 和等于k的 子数组的 个数即可
细节问题:
1.在计算 i 位置之前, 哈希表中只保存[0, i-1]区间的前缀和
2.不用真正创建一个前缀和数组, 因为dp[i]=dp[i-1]+nums[i],因此只要用一个变量sum标记i位置之前的前缀和即可, 计算出dp[i]之后,更新sum即可
3.有可能以i位置为结尾的数组中, 整个数组的和为k
因此我们提前把 前缀和0 和个数1建立映射关系
3.算法代码
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整除的子数组
974. 和可被 K 整除的子数组 - 力扣(LeetCode)https://leetcode.cn/problems/subarray-sums-divisible-by-k/1.题目解析
返回数组中 和 可被K整除的子数组(连续)的个数
2.算法分析
暴力解法和题目五基本是一样的~, 同样也不能用滑动窗口解决问题~
前缀和算法:
补充:
1.同余定理 (a-b) ÷ p = k -> a % p == b % p
2.C++中 负数 % 正数 结果是负数,因此我们如果想把结果变为正数 可以采取如下做法:
a % p ---> a % p + p 但是为了正数也能适用前面的式子,因此 (a % p + p) % p
前缀和思想和题目五也基本是一样的~
本题所采用的哈希表 存储的就是 前缀和的余数 和 个数 的映射关系
3.算法代码
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k)
{
unordered_map<int, int> hash; //统计前缀和余数出现的次数
hash[0 % k] = 1;
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;
}
};
七、连续数组
525. 连续数组 - 力扣(LeetCode)https://leetcode.cn/problems/contiguous-array/1.题目解析
题目要求找出0和1数量相等的最长连续子数组
转化:
1.将所有的0修改成-1
2.在数组中,找出最长的子数组,使子数组中所有元素的和为0
于是本题就转化成了题目五求和为k的子数组的问题了~
2.算法分析
本题与第五题的不同,也就是要注意的细节问题:
1.本题要求的是最长连续子数组的长度,不关心个数,因此哈希表中映射的是前缀和和下标j
2.如果哈希表中加入元素时发现已经有相同的前缀和了, 不需要更新,只需要保留前面的那一对<sum-k, j>, 因为题目求的是最长的连续子数组,因此不需要更新!
3. 前缀和为0的情况,存储的是 hash[0] = -1 (因为下标是-1)
4.和为0的子区间的长度
3.算法代码
class Solution {
public:
int findMaxLength(vector<int>& nums)
{
unordered_map<int, int> hash;
hash[0] = -1; //默认有1个前缀和为0的情况
int sum = 0, ret = 0;
for(int i = 0; i < nums.size(); i++)
{
sum += nums[i] == 0 ? -1 : 1; //将数组0变-1, 求前缀和
if(hash.count(sum)) ret = max(ret, i-hash[sum]); //hash[sum] == hash[sum-0]就是下标j
else hash[sum] = i; //sum不存在时,才将sum加入哈希表
}
return ret;
}
};
八、矩阵区域和
1314. 矩阵区域和 - 力扣(LeetCode)https://leetcode.cn/problems/matrix-block-sum/description/1.题目解析
2.算法分析
本题显然是题目二的应用,也就是二维前缀和思想
①ans[i][j]对应的要求和的区域在mat矩阵中是: [i-k, j-k] ~ [i+k, j+k]
但是要注意越界问题, 所以可以这么写:
x1 = max(0, i-k) ; y1 = max(0, j-k)
x2 = min(m-1, i+k) ; y2 = min(n-1, j+k)
②下标的映射关系
·dp矩阵下标是从(1, 1)开始,但原始矩阵mat下标从(0, 0)开始的,因此当我们要预处理
dp[x][y] 的时候,对应的mat矩阵的位置应该是 mat[x-1][y-1] 位置
·最终返回的ans数组下标也是从(0, 0)开始的,因此通过二维前缀和矩阵dp求解ans矩阵时,也要注意映射关系; 当我们求ans[x][y]位置时,对应的dp矩阵的位置应该是dp[x+1][y+1]位置
3.算法代码
class Solution
{
public:
vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k)
{
int m = mat.size(), n = mat[0].size();
//1.预处理前缀和矩阵
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]; //注意mat的下标
//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;
}
};