文章目录
- 455.分发饼干
- 思路
- 两个for循环嵌套的写法
- 为什么这种写法必须要有visited数组
- debug测试
- 逻辑问题:没有进行计数
- 逻辑问题:找到了result=3个孩子
- 一层for循环的写法
- 为什么这种写法一定要把小孩数组放在外面
- 376.摆动序列(逻辑问题)
- 思路
- 如何判断摆动
- 特殊样例:相邻元素相同
- 特殊样例:首尾元素
- 最开始的写法
- 第一次修改:卡住很久,最后发现是不能默认最左侧峰值存在的
- 正确修改
- 逻辑问题总结
455.分发饼干
- 重点在于掌握两层for循环嵌套的时候必须visited数组记录的问题,以及如何一层for循环解决满足条件才移动这种问题。
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i
,都有一个胃口值 g[i]
,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j
,都有一个尺寸 s[j]
。如果 s[j] >= g[i]
,我们可以将这个饼干 j
分配给孩子 i
,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
示例 2:
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.
提示:
- 1 <=
g.length
<= 3 * 10^4 - 0 <=
s.length
<= 3 * 10^4 - 1 <=
g[i], s[j]
<= 2^31 - 1
思路
输入两个数组分别是小孩的胃口g和饼干的大小s,饼干大小g[i]
必须>小孩子的胃口s[i]
才能满足小孩。
我们需要在数组g里面,满足尽量多的小孩,也就是较大的g[i]值,最好对应较大的s[i]值,才算不浪费g[i]。
因此,沿着这个思路分析,为了让尽可能多的g[i]得到满足(s[i]>=g[i]),每次的局部最优就是找到最大的s[i]
,也就是找到最大的饼干,先分给胃口最大的g[i]
。全局最优,就是我们可以喂饱最多的小孩子的数量。
当我们发现这个局部最优好像能够推出全局最优,同时又找不到反例的话,就可以尝试贪心。
大致思路:先找s数组里面较大的数值,再在g数组里面找满足s[i]>g[i]的最大数值。
两个for循环嵌套的写法
- 饼干的遍历需要条件进行控制,也就是说如果饼干过小满足不了小孩子的胃口,是不能继续遍历饼干的,但是需要继续遍历小孩子!
- 基于贪心的策略,我们优先保证大饼干供应g[i]比较大的孩子,因此sort可以直接降序排列,或者for循环倒着遍历
- 两个for循环嵌套的写法需要注意,由于两个for嵌套,每次外面的for循环数值加1,里面这层for都会从头开始遍历一遍,因此这种情况必须要统计里面的for哪些已经被遍历过了,否则就会反复遍历内层循环的同一个数值!这类似回溯递归中的i=startIndex的用法,为的就是不在内层遍历同样的元素!
//先对小孩胃口数组和饼干数组排序
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
//排序,默认升序
sort(g.begin(),g.end());
sort(s.begin(),s.end());
int result = 0;//记录喂饱了多少小孩
//两个for循环,记录小孩子已经被投食
vector<bool>visited(g.size(),false);
//开始遍历,孩子胃口从大到小遍历,饼干也从大到小遍历
//注意,如果饼干没有投喂成功的话,是不能向前遍历的!
for(int i=s.size()-1;i>=0;i--){
//小孩是内部循环,所以需要标记已经被遍历过的小孩子
for(int j=g.size()-1;j>=0;j--){
if(s[i]>=g[j]&&visited[j]!=true){
result +=1;
//标记这个小孩已经被投食
visited[j]=true;
break;
}
else{
continue;
}
}
}
return result;
}
};
为什么这种写法必须要有visited数组
两个for嵌套,每次外面的for循环数值加1,里面这层for都会从头开始遍历一遍。
因此这种情况,如果不想内层反复从头遍历一整遍,必须要统计里面的for哪些已经被遍历过了,否则就会反复遍历内层循环的同一个数值!这类似回溯递归中的i=startIndex的用法,为的就是不在内层遍历同样的元素!
例如下图的红色箭头和绿色箭头,有了红色箭头之后必须排除绿色箭头二次遍历的情况。
debug测试
逻辑问题:没有进行计数
最开始采用两层for循环的写法的时候,没意识到实际上每次外层变化,内层都会被从头开始遍历一遍!
因此内层为了不重复遍历,必须用visited数组来防止内层被遍历很多遍。加上数组之后就避免了内层重复遍历问题
逻辑问题:找到了result=3个孩子
在这种写法中,一个饼干找到了配套的孩子之后,内层孩子g的for循环没写break,没有break也就意味着一块饼干找到了孩子之后,不能及时遍历下一块饼干。
我们修改方式是在内层循环最后加上break。
一层for循环的写法
两层的写法实际上考虑复杂了,也可以只写一层for循环。
一层for循环的写法,就是针对小孩的胃口做遍历。
- 减少一层for循环的技巧在于,用了一个 index 来控制饼干数组的遍历,遍历饼干并没有再起一个 for 循环,而是采用自减的方式,这也是常用的技巧!
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
//排序,默认升序
sort(g.begin(),g.end());
sort(s.begin(),s.end());
int result = 0;//记录喂饱了多少小孩
int index = s.size()-1;
//开始遍历,孩子胃口从大到小遍历,饼干也从大到小遍历
for(int i=g.size()-1;i>=0;i--){
if(index>=0&&g[i]<=s[index]){
result += 1;
index--;
}
}
return result;
}
};
为什么这种写法一定要把小孩数组放在外面
因为index做指针这种for写法,外面的 for 里的下标 i 是固定移动的,而 if 里面的下标 index 是符合条件才移动的。
也就是说,下面这种情况,当饼干数组不符合条件的时候,小孩数组继续移动,但是饼干数组在不满足if条件的时候,index不变,也就是不移动!
输入: g = [1,2,3], s = [1,1]
输出: 1
这是不用两层for循环+continue的情况下,能够让饼干数组满足条件才移动的重要技巧!
那么为什么不能用for遍历饼干,if遍历小孩,示例如下:
if 里的 index 指向 胃口 10, for 里的 i 指向饼干 9,因为 饼干 9 满足不了 胃口 10,所以 i 持续向前移动,而 index 走不到s[index] >= g[i]
的逻辑,所以 index 不会移动,那么当 i 持续向前移动,最后所有的饼干都匹配不上。
所以 一定要 for 控制 胃口,里面的 if 控制饼干,防止这种第一个小孩胃口过大,结果所有的饼干遍历完了都没有能匹配的情况。
376.摆动序列(逻辑问题)
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5]
是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3)
是正负交替出现的。
相反,[1, 4, 7, 2, 5]
和 [1, 7, 4, 5, 5]
不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums
,返回 nums
中作为 摆动序列 的 最长子序列的长度 。
示例 1:
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
示例 2:
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。
示例 3:
输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2
思路
首先要理解摆动序列的含义,如下图所示。
相当于是通过删除元素,使得原有序列变为摆动子序列,并求出摆动子序列最大长度。
局部最优是删除单调坡度上的元素,使得这个坡度多出两个局部峰值;全局最优是峰值最多。
局部最优能推出全局最优,因此可以考虑贪心。
但是本题中我们不需要把多出来的元素都删掉!因为数组删除元素本身就是一个O(n)的操作(要先查询到元素位置),因此没必要真的做删除元素的操作!
实际上代码实现,只需要定义变量,遇到峰值做++,没有峰值就不做++即可。因此本题不用删除元素!删除元素会增加复杂度!
如何判断摆动
我们定义一个之前的差值pridiff = nums[i]-nums[i-1];
之后的差值curdiff = nums[i+1]-nums[i]
如果pridiff和curdiff一正一负,就说明是摆动,需要做++操作。只要节点两侧差值不一样,就说明这个节点是摆动峰值
if (prediff>0&&curdiff<0||prediff<0&&curdiff>0) result++;
特殊样例:相邻元素相同
题目中没有说相邻元素一定不同,因此还要考虑平坡情况,也就是pridiff = 0
或者curdiff= 0
。
如果只取平坡右边的值:
if (prediff=0&&curdiff>0||prediff=0&&curdiff<0) result++;
特殊样例:首尾元素
首尾元素都算作坡度,因此需要对首尾元素单独处理,因为首元素不存在prediff而末尾元素不存在curdiff
可以直接写死,遍历的时候不从第一个而是从第二个开始,并且不遍历最后一个,防止数组越界。
因为最开始和最后默认各有一个峰值,即使是前后相等,也不影响峰值的存在!
比如说下图蓝色字体的部分,即使数组前后加上了数字,也不影响峰值判断。
最开始的写法
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int prediff = 0;
int curdiff = 0;
if(nums.size()==1) return 1;
if(nums.size()==2){
if(nums[0]!=nums[1]) return 2;
else return 1;
}
int result = 2;//针对数组长度大于2的情况
for(int i=1;i<num.sise()-2;i++){
prediff = nums[i]-nums[i-1];
curdiff = nums[i+1]-nums[i];
//平坡取最右边元素,可以合并
if((prediff>=0&&curdiff<0)||(prediff<=0&&curdiff>0)){
result++;
}
}
return result;
}
};
这种写法忽略了一个问题,就是数组里所有的元素可能都相等!造成如下图逻辑问题,也就是说,我们的result初始值不能是2,必须是1.
result初始值不能是2,必须是1的话,意味着左右两个峰值,不能同时默认存在,需要在逻辑里加上首/尾峰值元素的判断。
因此针对这种写法做了以下修改:
第一次修改:卡住很久,最后发现是不能默认最左侧峰值存在的
- 因为数组里的元素可能都相等,所以初值result必须=1;此时,就需要单独有处理首尾的逻辑,否则首尾两个峰值一定会漏掉一个。
- 首尾两个峰值,从i=0开始还是i=1开始也很重要。下面这种写法从i=1开始,就是注定无解的。
- 必须从i=0开始,因为只有prediff能用curdiff来表示,而curdiff是不能用prediff来表示的!在数值往前遍历的过程中,prediff = curdiff,从而能够免去nums[i]-nums[i-1]的情况。
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int prediff = 0;
int curdiff = 0;
if(nums.size()==1) return 1;
if(nums.size()==2){
if(nums[0]!=nums[1]) return 2;
else return 1;
}
//针对数组长度>=3的情况,但是我们还要考虑,数组里全是相等元素的情况,所以result初值不能是2,应该是1
//默认序列最左边有一个峰值,最右边峰值通过curdiff修改来取,因为最右边没有i+1
int result = 1;
for(int i=1;i<=nums.size()-2;i++){
prediff = nums[i]-nums[i-1];
curdiff = nums[i+1]-nums[i];
//平坡取最右边元素,可以合并
if((prediff>=0&&curdiff<0)||(prediff<=0&&curdiff>0)){
result++;
}
}
return result;
}
};
正确修改
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int prediff = 0;
int curdiff = 0;
if(nums.size()==1) return 1;
if(nums.size()==2){
if(nums[0]!=nums[1]) return 2;
else return 1;
}
//针对数组长度>=3的情况,但是我们还要考虑,数组里全是相等元素的情况,所以result初值不能是2,应该是1
//默认序列最右边有一个峰值
int result = 1;
for(int i=0;i<=nums.size()-2;i++){
curdiff = nums[i+1]-nums[i];
if((prediff >= 0 && curdiff < 0) || (prediff <= 0 && curdiff > 0)){
result++;
prediff = curdiff;
}
}
return result;
}
};
逻辑问题总结
包含左侧峰值逻辑的示意图如下:
这道题的核心在于,pre可以继承cur的值,但是cur不能继承pre的值!卡住的原因就是因为没想明白这一点,从i=1开始遍历,无论怎样的继承方式,都不能把所有元素都遍历全。
因此在首尾两个峰值必须二选一留下来,也就是for循环,i=0还是i=1的选择时,必须从i=0开始,也就是默认初始存在的峰值是最右侧的峰值,而不是最左侧的峰值。