代码随想录算法训练营第三十三天| 455.分发饼干、376. 摆动序列 、53. 最大子序和

贪心算法理论基础

文档讲解:代码随想录

贪心的本质是选择每一阶段的局部最优,从而达到全局最优

这么说有点抽象,来举一个例子:

例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿?

指定每次拿最大的,最终结果就是拿走最大数额的钱。

每次拿最大的就是局部最优,最后拿走最大数额的钱就是推出全局最优。

再举一个例子如果是 有一堆盒子,你有一个背包体积为n,如何把背包尽可能装满,如果还每次选最大的盒子,就不行了。这时候就需要动态规划。动态规划的问题在下一个系列会详细讲解。

贪心算法并没有固定的套路

所以唯一的难点就是如何通过局部最优,推出整体最优。

那么如何能看出局部最优是否能推出整体最优呢?有没有什么固定策略或者套路呢?

也没有! 靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。

如何验证可不可以用贪心算法呢?

最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧

做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,就够了

455.分发饼干

文档讲解:代码随想录

题目链接:. - 力扣(LeetCode)

题目:

对每个孩子 i,都有一个胃口值  g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

思考:已知孩子的数量,孩子的胃口,以及饼干的数量,饼干的尺寸,要满足数量尽可能多的孩子,那么在每一步都满足一个孩子,将每个孩子的胃口值和饼敢尺寸排序,然后对比,前面的孩子就用比较小的饼干满足

充分利用饼干

局部最优:每一个孩子都用刚好能够满足他胃口的饼干,

大饼干满足大胃口的孩子:大饼干一定可以被吃,所以饼干不动,找到可以被吃的胃口大的孩子,饼干被吃掉后再移动饼干

或者是小饼干满足小胃口的孩子:小胃口的孩子一定有吃的,所以孩子不动,找到可以满足小胃口孩子的小饼干之后再移动孩子

两种写法如下:

###大饼干喂大孩子
class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        g.sort()
        s.sort()
        num = 0
        index = len(s) - 1 #饼干的下标,大饼干喂胃口大的孩子
        for i in range(len(g)-1,-1,-1): ##孩子在动
            
            ##饼干成功投喂才执行
            if  index >= 0 and s[index] >= g[i]:
                index = index - 1
                num = num + 1
        return num
###小饼干喂胃口小的孩子
class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        g.sort()
        s.sort()
        num = 0
        index = 0 #孩子的下标
        for i in range(0,len(s)): ##饼干在动,小饼干喂小胃口的孩子
            
            ##小胃口的孩子吃到才执行
            if  index < len(g) and s[i] >= g[index]:
                index = index + 1
                num = num + 1

        return num
            

376. 摆动序列 

文档讲解:代码随想录

题目链接:. - 力扣(LeetCode)

 不需要真的删除,只要判断出来摆动后,加1即可,明确摆动的条件,遇到摆动之后把值记录下来即可。如何判断摆动的条件是关键

局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值

整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列

局部最优推出全局最优,并举不出反例,那么试试贪心!

(为方便表述,以下说的峰值都是指局部峰值)

实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)

这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点

在计算是否有峰值的时候,大家知道遍历的下标 i ,计算 prediff(nums[i] - nums[i-1]) 和 curdiff(nums[i+1] - nums[i]),如果prediff < 0 && curdiff > 0 或者 prediff > 0 && curdiff < 0 此时就有波动就需要统计。

这是我们思考本题的一个大体思路,但本题要考虑三种情况:

  1. 情况一:上下坡中有平坡
  2. 情况二:数组首尾两端
  3. 情况三:单调坡中有平坡
情况一:上下坡中有平坡

例如 [1,2,2,2,1]这样的数组,如图:

 

它的摇摆序列长度是多少呢? 其实是长度是 3,也就是我们在删除的时候 要不删除左面的三个 2,要不就删除右边的三个 2。

如图,可以统一规则,删除左边的三个 2:

在图中,当 i 指向第一个 2 的时候,prediff > 0 && curdiff = 0 ,当 i 指向最后一个 2 的时候 prediff = 0 && curdiff < 0

如果我们采用,删左面三个 2 的规则,那么 当 prediff = 0 && curdiff < 0 也要记录一个峰值,因为他是把之前相同的元素都删掉留下的峰值。

所以我们记录峰值的条件应该是: (preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0),为什么这里允许 prediff == 0 ,就是为了 上面说的这种情况。

情况二:数组首尾两端

所以本题统计峰值的时候,数组最左面和最右面如何统计呢?

题目中说了,如果只有两个不同的元素,那摆动序列也是 2。

例如序列[2,5],如果靠统计差值来计算峰值个数就需要考虑数组最左面和最右面的特殊情况。

因为我们在计算 prediff(nums[i] - nums[i-1]) 和 curdiff(nums[i+1] - nums[i])的时候,至少需要三个数字才能计算,而数组只有两个数字。

这里我们可以写死,就是 如果只有两个元素,且元素不同,那么结果为 2。

不写死的话,如何和我们的判断规则结合在一起呢?

可以假设,数组最前面还有一个数字,那这个数字应该是什么呢?

之前我们在 讨论 情况一:相同数字连续 的时候, prediff = 0 ,curdiff < 0 或者 >0 也记为波谷。

那么为了规则统一,针对序列[2,5],可以假设为[2,2,5],这样它就有坡度了即 preDiff = 0,如图:

 

针对以上情形,result 初始为 1(默认最右面有一个峰值),此时 curDiff > 0 && preDiff <= 0,那么 result++(计算了左面的峰值),最后得到的 result 就是 2(峰值个数为 2 即摆动序列长度为 2) 

情况三:单调坡度有平坡

在版本一中,我们忽略了一种情况,即 如果在一个单调坡度上有平坡,例如[1,2,2,2,3,4],如图:

 

 

图中,我们可以看出,版本一的代码在三个地方记录峰值,但其实结果因为是 2,因为 单调中的平坡 不能算峰值(即摆动)。

之所以版本一会出问题,是因为我们实时更新了 prediff。

那么我们应该什么时候更新 prediff 呢?

我们只需要在 这个坡度 摆动变化的时候,更新 prediff 就行,这样 prediff 在 单调区间有平坡的时候 就不会发生变化,造成我们的误判。

本题异常情况的本质,就是要考虑平坡, 平坡分两种,一个是 上下中间有平坡,一个是单调有平坡,如图:

这道题目还是不太懂的 ,其实我们是假设已经删除了没有摆动的元素,所以prediff的变动肯定是在有摆动的时候的

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        if len(nums) == 1:
            return 1
        prediff = 0#默认前面延长一一段
        curdiff = 0
        result = 1 #默认最右端有坡度
        #判断第i个有没有波动
        for i in range(len(nums)-1): #最右端已经记录波动了
            curdiff = nums[i+1] - nums[i]
            if prediff >=0 and  curdiff <0: #两边不可以都有等于0,可能有平坡
                result = result + 1
                prediff = curdiff
            elif prediff <=0 and  curdiff >0:
                result = result + 1
                prediff = curdiff #一定要写在判断里面
        return result

53. 最大子序和

文档讲解:代码随想录

题目链接:. - 力扣(LeetCode)

思考:题目要求的是找到:具有最大和连续子数组(子数组最少包含一个元素),要找到连续的可以理解,但是问题是不知道要找的子数组中有多少个元素,有个想法就是以每个元素作为子数组的开始元素,往后叠加,直到遇到子数组和开始减少?但是刚开始减少的话,后面也可能会增加🤔,还有一种可能就是计算每一个元素与首元素组成的子数组的和,但是这种复杂度比较高n*n

贪心算法

贪心贪的是哪里呢?

如果 -2 1 在一起,计算起点的时候,一定是从 1 开始计算,因为负数只会拉低总和,这就是贪心贪的地方!

局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。

全局最优:选取最大“连续和”

局部最优的情况下,并记录最大的“连续和”,可以推出全局最优

从代码角度上来讲:遍历 nums,从头开始用 count 累积,如果 count 一旦加上 nums[i]变为负数,那么就应该从 nums[i+1]开始从 0 累积 count 了,因为已经变为负数的 count,只会拖累总和。

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        result = -float('inf') #初始化最大值为无穷小
        count = 0 #每一步的和
        for i in range(len(nums)):
            count += nums[i]
            if count > result:
                result = count
            if count < 0 : #count小于0再累加上后面的值,一定比0累加上后面的值小
                count = 0
        return result

常见误区

误区一:

不少同学认为 如果输入用例都是-1,或者 都是负数,这个贪心算法跑出来的结果是 0, 这是又一次证明脑洞模拟不靠谱的经典案例,建议大家把代码运行一下试一试,就知道了,也会理解 为什么 result 要初始化为最小负数了。

误区二:

大家在使用贪心算法求解本题,经常陷入的误区,就是分不清,是遇到 负数就选择起始位置,还是连续和为负选择起始位置。

在动画演示用,大家可以发现, 4,遇到 -1 的时候,我们依然累加了,为什么呢?

因为和为 3,只要连续和还是正数就会 对后面的元素 起到增大总和的作用。 所以只要连续和为正数我们就保留。

这里也会有录友疑惑,那 4 + -1 之后 不就变小了吗? 会不会错过 4 成为最大连续和的可能性?

其实并不会,因为还有一个变量 result 一直在更新 最大的连续和,只要有更大的连续和出现,result 就更新了,那么 result 已经把 4 更新了,后面 连续和变成 3,也不会对最后结果有影响。

动态规划的解法之后学到动态规划章节再补充

  • 29
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值