算法训练Day31 | 贪心算法理论基础;LeetCode455.分发饼干;376. 摆动序列;53. 最大子数组和

目录

贪心算法理论基础

 LeetCode455.分发饼干

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

 LeetCode376. 摆动序列

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

LeetCode53. 最大子数组和

方法一:暴力解法

方法二:贪心解法

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获


贪心算法理论基础

Referece:代码随想录 (programmercarl.com)

 LeetCode455.分发饼干

 链接:LeetCode455. 455. 分发饼干 - 力扣(LeetCode)

1. 思路

为了满足更多的小孩,就不要造成饼干尺寸的浪费。大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。

这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩

可以尝试使用贪心策略,先将饼干数组和小孩数组排序。然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。 

算法训练Day31 | 贪心算法理论基础;LeetCode455.分发饼干;376. 摆动序列;53. 最大子数组和_第1张图片

这个例子可以看出饼干9只有喂给胃口为7的小孩,这样才是整体最优解,并想不出反例,那么就可以写代码了;

2. 代码实现

实现思路1:优先分饼干,给饼干找合适的孩子

局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩;

优先把饼干喂出去,如果饼干满足不了当前的孩子,就换个胃口小的孩子,最后喂出去的饼干个数,就是满足的孩子的数量;

可以采用双指针法,初始都定义在数组的最后一位;然后根据情况向前移动;

# 优先把饼干分出去,把大饼干分给胃口大的
# 如果大饼干满足不了当前的孩子,就找个胃口更小的孩子分
# 最后饼干分出去的数量,就是满足的孩子的个数
# 双指针法+贪心
# time:**max( O(MlogM), O(NlogN) )**;space:O(1)
class Solution(object):
    def findContentChildren(self, g, s):
        """
        :type g: List[int]
        :type s: List[int]
        :rtype: int
        """
        g.sort()
        s.sort()
        child = len(g)-1
        cookie = len(s)-1
        count = 0
        while child>=0 and cookie>=0:
            # 如果当前饼干满足当前孩子
            if s[cookie]>=g[child]:
                count += 1
                cookie -= 1
                child -= 1
            # 如果当前饼干不满足当前孩子,找胃口更小的孩子
            else:
                child -= 1
        return count

也可以这样写,既然是优先分饼干,那就遍历孩子:

有的同学看到要遍历两个数组,就想到用两个for循环,那样逻辑其实就复杂了,可以用一个index来控制饼干数组的遍历,遍历饼干并没有再起一个for循环,而是采用自减的方式,这也是常用的技巧。

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        g.sort()
        s.sort()
        start, count = len(s) - 1, 0
        for index in range(len(g) - 1, -1, -1): 
            if start >= 0 and g[index] <= s[start]:
                start -= 1
                count += 1
        return count

实现思路2:优先喂饱孩子,给孩子找合适的饼干

局部最优就是小饼干喂给胃口小的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩;

优先把孩子满足,给胃口小的分小饼干,如果当前饼干满足不了当前孩子,就找个更大的饼干给他,最后孩子遍历到的位置,就是可以满足的孩子的数量;

可以采用双指针法,初始都定义在数组的第一位;然后根据情况向后移动;

# 优先把孩子满足,把小饼干分给胃口小的
# 如果当前饼干满足不了当前的孩子,就找个更大的饼干给他
# 最后孩子遍历到的位置,就是可以满足的孩子的数量
# 双指针法+贪心
# time:**max( O(MlogM), O(NlogN) )**;space:O(1)
class Solution(object):
    def findContentChildren(self, g, s):
        """
        :type g: List[int]
        :type s: List[int]
        :rtype: int
        """
        g.sort()
        s.sort()
        child = 0
        cookie = 0
        count = 0
        while child= g[child]:
                count += 1
                child += 1
                cookie += 1
            # 如果当前饼干不满足当前孩子,找更大的饼干
            else:
                cookie += 1
        return count

也可以这样写,既然是优先喂饱孩子,那就遍历饼干:

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        g.sort()
        s.sort()
        res = 0
        for i in range(len(s)):
            if res = g[res]:  #小饼干先喂饱小胃口
                res += 1
        return res

3. 复杂度分析

时间复杂度:max( O(MlogM), O(NlogN) )

M和N分别为孩子的个数和饼干的个数,排序算法就分别需要MlogM和NlogN;然后双指针遍历饼干和孩子,不超过O(M)和O(N),总体来说,复杂度为max( O(MlogM), O(NlogN) );

空间复杂度:O(1)

只使用了两个指针和一个结果变量;

4. 思考与收获

  1. 这道题是贪心很好的一道入门题目,思路还是比较容易想到的。
  2. 文中详细介绍了思考的过程,想清楚局部最优,想清楚全局最优,感觉局部最优是可以推出全局最优,并想不出反例,那么就试一试贪心

Reference: 代码随想录 (programmercarl.com)

本题学习时间:120分钟。


 LeetCode376. 摆动序列

 LeetCode376. 链接:376. 摆动序列 - 力扣(LeetCode)

1. 思路

本题要求通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

相信这么一说吓退不少同学,这要求最大摆动序列又可以修改数组,这得如何修改呢?来分析一下,要求删除元素使其达到最大摆动序列,应该删除什么元素呢?用示例二来举例,如图所示:

算法训练Day31 | 贪心算法理论基础;LeetCode455.分发饼干;376. 摆动序列;53. 最大子数组和_第2张图片

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

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

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

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

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

实现技巧

本题代码实现中,还有一些技巧,例如统计峰值的时候,数组最左面和最右面是最不好统计的。

例如序列[2,5],它的峰值数量是2,如果靠统计差值来计算峰值个数就需要考虑数组最左面和最右面的特殊情况。所以可以针对序列[2,5],可以假设为[2,2,5],这样它就有坡度了即preDiff = 0,如图:

算法训练Day31 | 贪心算法理论基础;LeetCode455.分发饼干;376. 摆动序列;53. 最大子数组和_第3张图片

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

2. 代码实现

具体实现思路是这样的,首先如果nums数组的长度为0或者为1,就直接返回该数组的长度就可以了,nums数组的长度≥2的前提下,进入以下逻辑:

首先定义preDiff = 0, 相当于在数组nums最前面,加上了一个与nums[0]大小相同的元素,然后curDiff也初始化为0,count=1,默认序列最右边有一个峰值;

然后开始遍历数组,pre和cur相乘为负数,就说明他们异号,满足序列的要求;

算法训练Day31 | 贪心算法理论基础;LeetCode455.分发饼干;376. 摆动序列;53. 最大子数组和_第4张图片

 如果有遇见元素相等的情况:

算法训练Day31 | 贪心算法理论基础;LeetCode455.分发饼干;376. 摆动序列;53. 最大子数组和_第5张图片

 代码实现如下:

# time:O(N);space:O(1)
class Solution(object):
    def wiggleMaxLength(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        # 如果nums为空,或者只有一个元素,返回0或者1
        if len(nums) <= 1: return len(nums)
        preDiff = 0 # 前一对元素的差
        curDiff = 0 # 当前一对元素的差
        # 记录序列中区间最值的个数,
        # 默认最后一个数是区间最值
        count = 1 
        for i in range(len(nums)-1):
            curDiff = nums[i+1]-nums[i]
						# cur差值为0时,不算摆动
            if preDiff * curDiff <= 0 and curDiff != 0:
                count += 1
# 如果当前差值和上一个差值为一正一负时,才需要用当前差值替代上一个差值
                preDiff = curDiff
        return count

3. 复杂度分析

  • 时间复杂度:O(n): 遍历了数组一次
  • 空间复杂度:O(1):常数个变量统计

4. 思考与收获

  1. 题目中其实说了nums长度大于等于1,当长度为1的时候,其实进不到for循环里面去,直接返回初始化的result=1;所以其实是不用考虑nums长度的;可以省略代码的第一行;

  2. 贪心的题目说简单有的时候就是常识,说难就难在都不知道该怎么用贪心

    本题大家如果要去模拟删除元素达到最长摆动子序列的过程,那指定绕里面去了,一时半会拔不出来。而这道题目有什么技巧说一下子能想到贪心么?其实也没有,类似的题目做过了就会想到。此时大家就应该了解了:保持区间波动,只需要把单调区间上的元素移除就可以了;

  3. (二刷再看)本题也可以用动态规划解

    考虑用动态规划的思想来解决这个问题。

    很容易可以发现,对于我们当前考虑的这个数,要么是作为山峰(即nums[i] > nums[i-1]),要么是作为山谷(即nums[i] < nums[i - 1])。

    • 设dp状态dp[i][0],表示考虑前i个数,第i个数作为山峰的摆动子序列的最长长度
    • 设dp状态dp[i][1],表示考虑前i个数,第i个数作为山谷的摆动子序列的最长长度

    则转移方程为:

    • dp[i][0] = max(dp[i][0], dp[j][1] + 1),其中0 < j < inums[j] < nums[i],表示将nums[i]接到前面某个山谷后面,作为山峰。
    • dp[i][1] = max(dp[i][1], dp[j][0] + 1),其中0 < j < inums[j] > nums[i],表示将nums[i]接到前面某个山峰后面,作为山谷。

    初始状态:

    由于一个数可以接到前面的某个数后面,也可以以自身为子序列的起点,所以初始状态为:dp[0][0] = dp[0][1] = 1

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

    进阶

    可以用两棵线段树来维护区间的最大值

    • 每次更新dp[i][0],则在tree1nums[i]位置值更新为dp[i][0]
    • 每次更新dp[i][1],则在tree2nums[i]位置值更新为dp[i][1]
    • 则dp转移方程中就没有必要j从0遍历到i-1,可以直接在线段树中查询指定区间的值即可。

    时间复杂度:O(nlog n)

    空间复杂度:O(n)

    动态规划

    class Solution:
        def wiggleMaxLength(self, nums: List[int]) -> int:
            # 0 i 作为波峰的最大长度
            # 1 i 作为波谷的最大长度
            # dp是一个列表,列表中每个元素是长度为 2 的列表
            dp = []
            for i in range(len(nums)):
                # 初始为[1, 1]
                dp.append([1, 1])
                for j in range(i):
                    # nums[i] 为波谷
                    if nums[j] > nums[i]:
                        dp[i][1] = max(dp[i][1], dp[j][0] + 1)
                    # nums[i] 为波峰
                    if nums[j] < nums[i]:
                        dp[i][0] = max(dp[i][0], dp[j][1] + 1)
            return max(dp[-1][0], dp[-1][1])
    
    

Reference·:代码随想录 (programmercarl.com)

本题学习时间:120分钟。


LeetCode53. 最大子数组和

LeetCode53. 链接:53. 最大子数组和 - 力扣(LeetCode)

方法一:暴力解法

暴力解法的思路,第一层for 就是设置起始位置,第二层for循环遍历数组寻找最大值;Python在LeetCode上提交会超时;

# 暴力解法
# 执行代码可以过,提交代码超时
# time:O(N^2);space:O(1)
class Solution(object):
    def maxSubArray(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        maxSum = -float("inf")
        maxLen = 0
        curSum = 0
        curLen = 0
        for i in range(len(nums)):
            curLen = 0
            curSum = 0
            for j in range(i,len(nums)):
                curLen += 1
                curSum += nums[j]
                if curSum > maxSum:
                    maxSum = curSum
                    maxLen = curLen
        return maxSum

方法二:贪心解法

1. 思路

贪心贪的是哪里呢?

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

局部最优推出全局最优?

  1. 局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
  2. 全局最优:选取最大“连续和”
  3. 局部最优的情况下,并记录最大的“连续和”,可以推出全局最优

在代码中要如何实现?

从代码角度上来讲:遍历nums,从头开始用count累积,如果count一旦加上nums[i]变为负数,那么就应该从nums[i+1]开始从0累积count了,因为已经变为负数的count,只会拖累总和。这相当于是暴力解法中的不断调整最大子序和区间的起始位置;每次取count为正数的时候,开始一个区间的统计;

**区间终止位置不用调整么? 如何才能得到最大“连续和”呢?**如果count取到最大值了,及时记录下来!

2. 代码实现

# 贪心解法
# time:O(N);space:O(1)
class Solution(object):
    def maxSubArray(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        result = -float("inf")
        curSum = 0
        for i in range (len(nums)):
            curSum += nums[i]
            # 取区间累计的最大值(相当于不断确定最大子序终止位置)
            if curSum > result:
                result = curSum
            # 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
            if curSum <0:
                curSum =0
        return result

3. 复杂度分析

时间复杂度:O(N); 需要遍历数组一遍

空间复杂度:O(1);常数个变量记录

4. 思考与收获

  1. 当然题目没有说如果数组为空,应该返回什么,所以数组为空的话返回啥都可以了;

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

  3. 本题的贪心思路其实并不好想,这也进一步验证了,别看贪心理论很直白,有时候看似是常识,但贪心的题目一点都不简单!

  4. (二刷再看)动态规划做法

    当然本题还可以用动态规划来做,当前主要讲解贪心系列,后续到动态规划系列的时候会详细讲解本题的dp方法。

    那么先给出我的dp代码如下,有时间的录友可以提前做一做:

    class Solution {
    public:
        int maxSubArray(vector& nums) {
            if (nums.size() == 0) return 0;
            vector dp(nums.size(), 0); // dp[i]表示包括i之前的最大连续子序列和
            dp[0] = nums[0];
            int result = dp[0];
            for (int i = 1; i < nums.size(); i++) {
                dp[i] = max(dp[i - 1] + nums[i], nums[i]); // 状态转移公式
                if (dp[i] > result) result = dp[i]; // result 保存dp[i]的最大值
            }
            return result;
        }
    };
    
    • 时间复杂度:O(n)
    • 空间复杂度:O(n)

Reference: 代码随想录 (programmercarl.com)

本题学习时间:40分钟。


本篇学习时间近5小时,总结字数近7000;主要做了贪心算法的几道题,贪心题目的唯一共同点就是保证局部最优推出整体最优,其他的毫无规律可言,还是比较难的呀!(求推荐!)

你可能感兴趣的:(代码随想录训练营,算法,贪心算法,leetcode)