Bootstrap

代码随想录算法训练营第二十六天 | 贪心算法理论基础 455.分发饼干 376.摆动序列 53.最大子序和

贪心算法理论基础:

① 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
② 一般能够手动模拟感觉到局部最优可以推出全局最优,没有找到反例,就可以用贪心算法。
③ 比如,一堆东西中可以选择4个东西,每次选择当时这堆东西中最贵的,那么最后得到的就是最贵的。
④ 而背包问题:背包容积固定,一堆东西向背包中装,求最后得到的背包所装东西的价值最大,那么不应该采用贪心,而是动态规划


LeetCode 455.分发饼干:

文章链接
题目链接:455.分发饼干

思路:

贪心算法,要么从大饼干开始,大饼干先满足胃口大的孩子;要么从小饼干开始,小饼干先满足胃口小的孩子

"""
大饼干先满足胃口大的孩子
先遍历胃口,再遍历饼干
如果g[i] > s[-1],也就是g[i] > max(s),所以胃口g[i]一定满足不了
因此只需要if判断即可
实际上是对于每个饼干,能不能找到一个合适的胃口与之对应
"""
class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        if s == []:
            return 0
        g.sort(reverse=True)
        s.sort()
        count = 0
        for i in range(len(g)):
            if s!=[] and g[i] <= s[-1]:
                count += 1
                s.pop()
            else:
                continue
        return count
       
"""
大饼干先满足大胃口的孩子
先遍历饼干,再遍历胃口。
因为先遍历的饼干,如果sj < g[-1],但是可能在g[:-1]中找到饼干sj对应的胃口
所以需要嵌套for循环找到饼干对应的胃口
""" 
class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        if s == []:
            return 0
        g.sort()
        s.sort(reverse=True)
        count = 0
        for sj in s:
            while g != [] and sj < g[-1]:
                g.pop()
            if g !=[]:
                count += 1
                g.pop()
        return count


"""
小饼干先满足小胃口。
先遍历饼干,再遍历胃口
如果s[i] < g[index],表明这个饼干确实没有对应满足的胃口
所以可以直接使用if判断
"""
class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        if s == []:
            return 0
        g.sort()
        s.sort()
        index = 0
        for sj in s:    # 遍历饼干
            if index < len(g) and g[index] <= sj:
                index += 1
        return index  

感悟:

大饼干先满足大胃口与小饼干先满足小胃口,其中的遍历顺序:
大饼干对应的遍历顺序是先胃口后饼干(因为如果大胃口比大饼干要大,那么该大胃口一定不会满足)
小饼干对应的遍历顺序为先饼干后胃口(因为如果小饼干比小胃口要小,那么该小饼干找不到对应满足的胃口)


LeetCode 376.摆动序列:

文章链接
题目链接:376.摆动序列

思路:

  1. 贪心
    可以先把原数组的摆动情况画出来,以[1, 17, 5, 10, 13, 15, 10, 5, 16, 8]为例
    在这里插入图片描述
    从上图可以看出,原数组中的元素,有些为波峰和波谷,有些不是波峰波谷。
    题目要求从原数组中找到作为摆动序列的最长子序列,而摆动序列画出来的图中的点都是波峰或者波谷,所以我们可以只去掉原序列中非波峰波谷的点,从而得到摆动序列。
    再进一步,只要求最长子序列长度,那么我们可以实际上可以只记录波峰波谷的数量。

    • 怎么判断为波峰波谷:
      使用preval和curval,分别记录nums[i] - nums[i - 1]和nums[i + 1] - nums[i]的值 ,

      只要preval > 0 && curval < 0 或preval < 0 && curval > 0 ,那么就表明为波谷或波峰

    • 题目中没有说数组元素不能重复,因此会有重复的元素,也就是平坡出现。根据平坡右边和左边的上下情况,有如下几种情形

      1. 左右相反。左边上右边下(即平坡是最高的),左边下右边上(即平坡是最低的)。以第一种为例
        在这里插入图片描述
        上图中,我们既可以去掉最右边两个10,也可以去掉最左边两个10,我们采用去掉最左边两个10的方式,那么最后一个10也是一个波峰,此处pre = = 0 && cur < 0 \\\\(2)。
        同理可得,左边下右边上同样保留最右边的情况为pre == 0 && cur > 0 \\\\(3)

      2. 左右相同,即都为上或都为下,此时平坡为整个上坡或下坡的一部分。
        在这里插入图片描述
        在上图中,平坡中的元素都不应该作为波峰或波谷存在。那么可以看出最后一个10的preval和curval应当为preval > 0 and curval > 0才能不将10作为波峰和波谷,之前的最后一个10的preval和curval为,preval = 0 and curval > 0。那么办法是,只在出现摆动时更新preval

    • 数组最左/右边的元素
      前面使用preval和curval都是当前元素的左右都存在元素时进行判断,那么我们还需要对数组的边界元素进行判断。
      怎么对边界元素进行判断呢:

      1. 最右边的元素:
        如果左边为平坡过来的,那么这个实际上是按照平坡的左右不等来计算的,因此最右边应当作为波峰/波谷
        如果左边不是平坡,那么最右边元素也是波峰/波谷。
        因此不管怎么样,最右边元素都会作为最长摆动子序列的一个部分。
        在这里插入图片描述
      2. 最左边元素:
        如果右边为平坡,那么最左边元素不作为波峰/波谷。
        如果右边不为平坡,那么最左边元素为波峰/波谷。
        那么这个判断与正常具有左右元素的判断相同,因此我们可以采用在最左边元素前添加一个相同的元素作为nums[i - 1],从而判断最左边元素与正常元素相同
        在这里插入图片描述
class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        lenNums = len(nums)
        if lenNums <= 1:
            return lenNums
        preval = 0  # nums[i]与nums[i-1]之间的差值,对于第一个元素,那么就是在前面加上了一个相同的元素,从而preval=0
        curval = 0  # nums[i]与nums[i+1]之间的差值
        result = 1  # 默认数组最右边是一个波峰
        for i in range(lenNums - 1):
            curval = nums[i + 1] - nums[i]
            if (preval <= 0 and curval > 0) or (preval >= 0 and curval <0):
                result += 1
                preval = curval # 摆动时才更新preval
        return result   
  1. 动态规划:
    可以将nums[i]作为波峰或波谷,接到前面的最长摆动序列中
    将nums[i]作为波峰接到前面的摆动序列中,摆动序列前一个元素应当为波谷
    dp[i][0] = max(dp[i][0], dp[j][1] + 1)
    将nums[i]作为波谷接到前面的摆动序列中,摆动序列前一个元素应当为波峰
class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        lenNums = len(nums)
        if lenNums <= 1:
            return lenNums
        # 对于第i行元素, 0列表示将nums[i]作为波峰的最大摆动序列长度,1列表示将nums[i]作为波谷的最大摆动序列长度
        dp = [[0,0] for _ in range(lenNums)]
        dp[0] = [1,1]
        # 每次确定一行的元素
        for i in range(1, lenNums):
            dp[i][0], dp[i][1] = 1, 1  # 自己
            
            for j in range(i):
                # 波峰
                if nums[j] < nums[i]:
                    dp[i][0] = max(dp[i][0], dp[j][1] + 1)  #dp[i][0]实际上是找最大值,nums[i]作为波峰,那么前一个元素应当为波谷
                # 波谷
                if nums[j] > nums[i]:
                    dp[i][1] = max(dp[i][1], dp[j][0] + 1)  # nums[i]作为波谷,前面一个元素作为波峰
        return max(dp[-1][0], dp[-1][1])
        

时间复杂度O(n^2)
空间复杂度O(n)

感悟:

摆动序列使用贪心算法的各种情况及解决办法


LeetCode 53.最大子序和:

文章链接
题目链接:53.最大子序和

思路:

  1. 动态规划
    如果采用dp[i]为nums[:i + 1]的最大连续子数组的和,那么dp[i + 1]与dp[i]衔接非常麻烦,因为要求的子数组为连续子数组
    因此采用dp[i]为nums[: i + 1]的包括nums[i]的最大连续子数组的和,从而dp[i + 1] = max(nums[i + 1], dp[i] + nums[i + 1])。最后再遍历一遍dp得到最大值即可
    以[-2, 1, -3, 4, -1, 2, 1, -5, 4]数组为例,构造dp如下图所示:
    在这里插入图片描述
"""
可以再优化一下,求dp数组最大值可以在构建dp时处理
"""
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        lenNums = len(nums)
        if lenNums == 1:
            return nums[0]
        dp = [0] * lenNums  # dp[i]为nums[:i + 1]中且包含nums[i]的最大连续子数组的和
        dp[0] = nums[0]
        for i in range(1, lenNums):
            dp[i] = nums[i]
            if nums[i] < dp[i - 1] + nums[i]:
                dp[i] = dp[i - 1] + nums[i]
        # 遍历dp数组,找到其中的最大值即为所求
        maxIndex = 0
        for i in range(1, lenNums):
            if dp[i] > dp[maxIndex]:
                maxIndex = i
        return dp[maxIndex]     
  1. 贪心
    由上面的动态规划可以发展出贪心,使用count记录nums[:i]的包括nums[i - 1]的连续和。
    若count < 0: count = nums[i] # 从nums[i]开始
    否则:count += nums[i] # 增加nums[i]的值
    PS:不是nums[i] < 0就不加,而是count < 0就重新开始,因为要求的是连续子数组,count记录的是包含nums[i]的最大连续子数组,所以nums[i]一定要带上
    同时使用result记录count的最大值
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        lenNums = len(nums)
        if lenNums == 1:
            return nums[0]
        result = nums[0]  # 记录最大值
        count = nums[0] # 连续和
        for i in range(1, lenNums):
            if count < 0:   #  为负数的话,count重新开始
                count = nums[i]
            else:
                count += nums[i]    # 否则加上nums[i]
            if count > result:
                result = count
        return result

        

感悟:

注意到题目中的最大连续子数组


学习收获:

贪心主要是不知道在哪里贪心。
分发饼干可以明确知道,摆动序列的贪心算法浅浅过了一遍,最大子序和先想到的用动态规划,再把动态规划转成贪心。

;