DAY35:贪心算法(二)分发饼干+摆动序列

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开始,也就是默认初始存在的峰值是最右侧的峰值,而不是最左侧的峰值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值