第一次复习时间:09-27
文章目录
结论
- 简单的说前缀和是思想,从3层循环暴力 减少到2层,哈希表是优化,从二层循环减少到一层
- 0927补充:
- 简单的说前缀表就是把问题转换成
两数之差 会不会等于 k 情况的判断。
我们之前做过两次for循环求两数之和的题 通过hash减少时间复杂度 这题同理
特别的在于 前缀表的定义决定写法,如果下标表示长度 add(n+1) 长度从0开始 那么任意两数相减就可以遍历所有的情况 如果下标表示实际坐标 那么还要还要考虑 a-b的a本身
实例代码560
当
提到连续的数组我们很容易想到是否可以用到前缀和
或者 滑动空间的思想去解决,但是滑动窗口更严格,比如递增或者递减序列,或者能满足那种左右指针向一边移动能够固定增减的。
补充
基本步骤 先构建前缀树的放入一维数组 然后构建map 让map【0】=1 一个for循环,不断要把前缀树的数据放入 但是在放入之前需要判断当前-所求在不在(举个例子求判断是否存在k 已知k+b=当前 那我们知道当前和k 完全可以把问题转换成 判断b在不在)。map第一个值表示(可能表示前缀和 也有特殊比如那个奇数个数 值表示频率)。前缀和有时候我们都可以省略,只要在判断直接算出当前前缀和就好了
前缀和例题
724 寻找数组的中心索引(简单)
- 就像之前说的 连续的子序列 二叉树的路径 都可以思考前缀和解法。
- 方法一:前缀和
- 方法二:双指针
class Solution {
public:
int pivotIndex(vector<int> &nums) {
int total = accumulate(nums.begin(), nums.end(), 0);
int sum = 0;
for (int i = 0; i < nums.size(); ++i) {
if (2 * sum + nums[i] == total) {
return i;
}
sum += nums[i];
}
return -1;
}
};
1248 统计优美的子数组(中等)(前缀和+hash)
- 很常见的之和 之差 都可以加上hash减少一层的遍历(比如经典的两数之和)
- 第一遍写的没有节省空间,第二遍把空间优化了一下
class Solution {
public:
int numberOfSubarrays(vector<int>& nums, int k)
{
int n=nums.size();
unordered_map<int,int> map;//键表示的奇数的个数 值表示频率
map[0]=1;
int count=0;
vector<int> vec(n);
int sum=0;
for(int i=0;i<n;i++)
{
if(nums[i]&1 == 1)
{
sum=sum+1;
vec[i]=sum;
}
else
{
vec[i]=sum;
}
if(map.count(vec[i]-k)==1)
{
count=count+map[vec[i]-k];
}
map[vec[i]]++;
}
return count;
}
};
class Solution {
public:
int numberOfSubarrays(vector<int>& nums, int k)
{
int n=nums.size();
unordered_map<int,int> map;//键表示的奇数的个数 值表示频率
map[0]=1;
int count=0;
//vector<int> vec(n);
int res;
int sum=0;
for(int i=0;i<n;i++)
{
if(nums[i]%2 == 1)//判断是不是奇数
{
sum=sum+1;//前缀和 在判断前计算就好 不用数组
}
if(map.count(sum-k)==1)//判断是否存在
{
count=count+map[sum-k];
}
map[sum]++;//挂上去
}
return count;
}
};
560 和为k的子数组(中等)(前缀+哈希)
- 看到连续子数组 第一个想法就是滑动窗口…或许先来个排序 试试看吧,写完发现错了…这不能修改数组的顺序 要么怎么叫连续子数组呢。。想的太简单了
-
简单的说前缀和是思想,从3层循环暴力 减少到2层,哈希表是优化,从二层循环减少到一层
-
简单的说前缀表就是把问题转换成
两数之差 会不会等于 k 情况的判断。
-
我们之前做过两次for循环求两数之和的题 通过hash减少时间复杂度 这题同理
-
特别的在于 前缀表的定义决定写法,如果下标表示长度 add(n+1) 长度从0开始 那么任意两数相减就可以遍历所有的情况 如果下标表示实际坐标 那么还要还要考虑 a-b的a本身
特别重要 注意下面 1 2代码的区别 -
当然代码还可以简洁 前缀表的空间还可以省去 代码一的改进,两个并列for循环可以同时进行 修改为第三个代码
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int n=nums.size();
vector<int>add(n,0);
add[0]=nums[0];
for(int i=1;i < n;i++)add[i]=add[i-1]+nums[i];
int ans=0;
unordered_map<int,int> map;
map[0]=1;//这边是判断等于本身的情况
for(int i=0; i<n ;i++)
{
if(map.count(add[i]-k))ans+=map[add[i]-k];
map[add[i]]++;
}
return ans;
}
};
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int n=nums.size();
vector<int>add(n+1,0);
//add[0]=nums[0];
for(int i=1;i <= n;i++)add[i]=add[i-1]+nums[i-1];
int ans=0;
unordered_map<int,int> map;
for(int i=0; i<=n ;i++)
{
if(map.count(add[i]-k))ans+=map[add[i]-k];
map[add[i]]++;
}
return ans;
}
};
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int n=nums.size();
unordered_map<int,int> map;
int pre = 0;
int count =0;
map[0] = 1;
for(int i=0; i<n ;i++)
{
pre =pre+nums[i];
if(map.count(pre-k)==1)
{
count+=map[pre-k];
}
map[pre]++;
}
return count;
}
};
- 没啥问题,代码一遍过
974 和可被k整除的子数组(中等)(巧)
- 唯一的不同在于,键保存的是余数而且要特别处理小于0的余数,这个很关键。因为我们是是利用判断前缀和的余数相同 那么他们的i到j的子数组就能被整除
本题将负数进行同余预处理。如给出:[2,-2,2,-4],K=6。同余处理之后得到[2,4,2,2],K=6,求余数之后为[0,2,0,2,4],余数相等的有两对,答案是2.
0是可以整除任何数也是任何数的倍数
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k)
{
int n=nums.size();
unordered_map<int,int> map;
map[0]=1;
int count=0;
int pre = 0;//这个用来保存余数
for(int i=0;i<n;i++)
{
pre=(nums[i]+pre) % k;
if(pre < 0 )pre=pre+k;//同于处理
if(map.count(pre)==1) //(b-a)%k =0 等价与 b%k =a%b 所以就变成找余数相同的前缀和
{
count=count+map[pre];
}
map[pre]++;
}
return count;
}
};
523 连续的子数组和(中等)
- 这个题看似和上一题一样,但是要求数组长度大于=2让我改了非常久,首先我想的是值不用来保存频率了,那索性保存下标,这样就可以达到两个判断。
但是如下第一个代码一直有测试案例无法通过 【5,0,0,0】3。那是因为我每次遇到相同余数都回去修改他的值数组的长度一致变大,所以只差始终无法大于等于2,所以我们当map不存在再去保存这个键值,否则始终使用第一次出现的下标,这样子就可以达到最远的两个距离进行判断
,
class Solution {
public:
bool checkSubarraySum(vector<int>& nums, int k) {
int n=nums.size();
unordered_map<int,int> map;
map[0]=-1;
int pre = 0;//这个用来保存余数
for(int i=0;i<n;i++)
{
pre=(nums[i]+pre) % k;
if(map.count(pre)==1 ) //(b-a)%k =0 等价与 b%k =a%b 所以就变成找余数相同的前缀和
{
if(i-map[pre]>=2)
{
return true;
}
}
map[pre]=i;
}
return false;
}
};
930 和相同的二元子数组(中等)
- 没有什么区别
- 补充一个点,这个题可以用滑动窗口,因为确定的一个点就是右边界右移一定不会减少,左边界右移一定不会增大
class Solution {
public:
int numSubarraysWithSum(vector<int>& nums, int goal)
{
unordered_map<int,int> map;
map[0]=1;
int n = nums.size();
int pre=0;
int count=0;
for(int i=0;i<n;i++)
{
pre=pre+nums[i];
if(map.count(pre-goal)==1)
{
count+=map[pre-goal];
}
map[pre]++;
}
return count;
}
};
437 路劲之和3(中等)
-
题目分为4种
- 从根结点到页节点
- 从根节点到任意节点
- 从任意节点到叶结点
- 从父子节点到子节点
-
第一眼看到这个题目,其实没有想到前缀和+回溯来解决。
但是看了题解很容易理解,如果想到用前缀和来解题(单独思考一条路劲),然后再想到如果想到从左子树返回到父子节点再进入右子树需要修改当前前准和 和 map的值就可以想到用回溯
-
和之前做的一个题很像,我们只要知道有几条路径,不需要知道具体的路劲,所以键是sum 值是数量
-
老样子要么一开始定义路径为0的数量为1,这样是为了找到本身,之前讲过
-
回溯什么东西么1 当你从下一层退回到上一层首先前缀和需要修改回去,第二个就是map对于当前层的前缀和减去
认真思考
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
unordered_map<int,int> map;//键表示sum综合 值表示数量
int res=0;//保存结果
//先序处理
void dfs(TreeNode* root,int& sum,int& cur_sum)
{
//递归终止条件
if(root==nullptr) return;
//处理当前层的逻辑
//1更新当前层的前缀和
cur_sum=cur_sum+root->val;
//2判断有没有符合条件的 有就统计结果 没有啥事没做
if(map.count(cur_sum-sum)==1)
res =res+map[cur_sum-sum];
//3 不管有没有符合的 把当前前缀和 放入map
map[cur_sum]++;
//4递归
dfs(root->left,sum,cur_sum);
dfs(root->right,sum,cur_sum);
//5回溯
map[cur_sum]--;
cur_sum=cur_sum-root->val;
return;
}
int pathSum(TreeNode* root, int targetSum)
{
map[0]=1;//表示本身为结果 不是差为结果
int cur_sum=0;
dfs(root,targetSum,cur_sum);
return res;
}
};
如有错误,欢迎指出,本文以个人记录复习为主。