【代码随想录】贪心算法

本文详细介绍了贪心算法在解决分发饼干、摆动序列计数、最大子数组和、股票交易等实际问题中的应用,强调了贪心策略的核心思想,以及在编程实现中需要注意的问题和优化点。
摘要由CSDN通过智能技术生成

贪心算法

没有框架,每题都是局部最优推出全局最优,只要想不出反例,就可以证明可行。

455. 分发饼干

我用了小饼干先满足小胃口的思路。

  • 这个思路要for循环遍历饼干,if里面判断饼干能不能满足胃口。因为排序后,已经用最小的饼干去匹配最小的胃口了,如果不能满足,只能换下一个饼干,而不是换下一个胃口。
  • 要判断 index<len(g),不然超界了。
  • 为什么时间复杂度是O(nlogn)?

376.摆动序列

本题也可以用动态规划方法。一刷先不用。

  1. 思路解析:
    局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
    整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
  • 实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)。
  • 这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点。
  • 在计算是否有峰值的时候,大家知道遍历的下标 i ,计算 prediff(nums[i] - nums[i-1])curdiff(nums[i+1] - nums[i]),如果prediff < 0 && curdiff > 0 或者 prediff > 0 && curdiff < 0 此时就有波动就需要统计。
  1. 需考虑3种情况:
  • 上下坡中有平坡
    在这里插入图片描述
    • 解决方法:统一采用删左面三个 2 的规则,那么 当 prediff = 0 && curdiff < 0 也要记录一个峰值,因为他是把之前相同的元素都删掉留下的峰值。
      所以我们记录峰值的条件应该是: (preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)
    • 注意:这里两个坡只记录一个峰值,也就相当于删除了前面多余的2,而不是两个坡都删除掉了。
  • 数组首尾两端
    在这里插入图片描述
    这里我们可以写死,就是 如果只有两个元素,且元素不同,那么结果为 2。

不写死的话,如何和我们的判断规则结合在一起呢?
可以假设,数组最前面还有一个数字,那这个数字应该是什么呢?
之前我们在 讨论 情况一:相同数字连续 的时候, prediff = 0curdiff < 0 或者 >0 也记为波谷。
那么为了规则统一,针对序列[2,5],可以假设为[2,2,5],这样它就有坡度了即 preDiff = 0
针对以上情形,result 初始为 1(默认最右面有一个峰值),此时 curDiff > 0 && preDiff <= 0,那么 result++(计算了左面的峰值),最后得到的 result 就是 2(峰值个数为 2 即摆动序列长度为 2)

// 版本一
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if (nums.size() <= 1) return nums.size();
        int curDiff = 0; // 当前一对差值
        int preDiff = 0; // 前一对差值
        int result = 1;  // 记录峰值个数,序列默认序列最右边有一个峰值
        for (int i = 0; i < nums.size() - 1; i++) {
            curDiff = nums[i + 1] - nums[i];
            // 出现峰值
            if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) {
                result++;
            }
            preDiff = curDiff;
        }
        return result;
    }
};

问题1:这里为什么要预留有一个result(初始值为1),就是为了处理数组长度为2的情况吗?品一下,能想出来真的很厉害。
问题2:数组长度为2的时候,是怎么假设前面有一个相同的数字的?让prediff = curdiff吗?

  • 单调坡中有平坡
    解决方法:
    在这里插入图片描述
    图中,我们可以看出,版本一的代码在三个地方记录峰值,但其实结果因为是 2,因为 单调中的平坡 不能算峰值(即摆动)。
    之所以版本一会出问题,是因为我们实时更新了 prediff。

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

  • 代码遇到的问题:
  • 错把cur_diff = nums[i+1] - nums[i]写成了cur_diff = nums[i] - nums[i-1],导致case 3这种单调递增的情况,没有通过

53. 最大子数组和

几种解法:

  1. 暴力解法:
  • 将result初始化为负无穷:result = float('-inf')
  • 需要两个循环,第一个确定数组初始位置,第二个在初始位置之后遍历,每次确定过初始位置后count都要重置为0
def maxSubArray(self, nums: List[int]) -> int:
	result = float('-inf')
	count = 0
	
	for i in range(len(nums)):
	    count = 0
	    for j in range(i, len(nums)):
	        count += nums[j]
	        result = max(count, result)
	
	return result
  1. 贪心解法
  • 贪心贪的是哪里呢?
    如果 -2 1 在一起,计算起点的时候,一定是从 1 开始计算,因为负数只会拉低总和,这就是贪心贪的地方!
    • 局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
    • 全局最优:选取最大“连续和”
      局部最优的情况下,并记录最大的“连续和”,可以推出全局最优。
      这相当于是暴力解法中的不断调整最大子序和区间的起始位置。

那有同学问了,区间终止位置不用调整么? 如何才能得到最大“连续和”呢?
区间的终止位置,其实就是如果 count 取到最大值了,及时记录下来了。

  • 为什么当count < 0的时候开始重新计算,而不是遇到nums[i] < 0开始重新计算?——我觉得是因为贪心算法相当于对暴力算法的剪枝,如果遇到nums[i] < 0就开始重新计算可能会导致错失了本应有得最大子序和,而遇到count < 0开始计算,此时一定不会遇到最大子序和了。
  • 代码遇到的问题:
    • 并不是count>0的时候,更新result让result = count,而是当count > result的时候,更新result让result = count,更不是让result=max(count, result)不然会发生当nums数组里面全是负数的时候,返回result=0的情况,因为count已经初始化为0了

122. 买卖股票的最佳时机 II

  1. 解题思路:利润是可以分解的。
  2. 代码随想录里面的解法是对的,不需要考虑两个正数是不是连着,因为连着也正好,在正数出现的前一天买入,在正数出现的最后一天卖出即可。

55. 跳跃游戏

  1. 解题思路:其实跳几步无所谓,关键在于可跳的覆盖范围!
    不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。
    这个范围内,别管是怎么跳的,反正一定可以跳过来。
    那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!
    每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。
    贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
  2. 代码遇到问题:
  • cover = max(i + nums[i], cover)是对的,cover = i + nums[i] 这样是不对的,有些元素0很多的情况会通不过,比如nums=[3,0,8,2,0,0,1]

45. 跳跃游戏 II

  1. 解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!
    这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖。
  2. 精华部分|理解难点
    从下标0的位置记录下覆盖范围是2之后,计算下一步的覆盖范围不是直接记录下标为2位置的,而是记录下标0的覆盖范围内途经的最大覆盖范围,下标2的覆盖范围没有下标1的覆盖范围大,因此用下标1的覆盖范围。用max(i+nums[i], next)表示。
    在这里插入图片描述
  3. 什么时候启动下一步覆盖范围:当前下标的覆盖范围走完了,而且还没走到终点。也就是启动下一步的最大覆盖范围。
  4. 用的代码随想录方法1.——要搞懂代码每一步怎么运作的,按想法步骤写出来就好了

1005. K 次取反后最大化的数组和

  1. 贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。
    那么如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。
    那么又是一个贪心:局部最优:只找数值最小的正整数进行反转,当前数值和可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和 达到最大。
  2. 那么本题的解题步骤为:
    第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
    第二步:从前向后遍历,遇到负数将其变为正数,同时K–
    第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
    第四步:求和
  3. 时间复杂度: O(nlogn)——为什么?
  4. 自己写的代码出现的错误:
  • 用while k很容易超出时间限制
    def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
        nums.sort(key = lambda x: abs(x), reverse = True)

        while k:
            for i in range(len(nums)):
                if nums[i] < 0:
                    nums[i] = -nums[i]
                    k -= 1
            if k % 2 == 1:
                nums[-1] *= -1
                break

        return sum(nums)

这样写从代码思路上面来说是没错的,但会超出时间限制。思考下为什么:——每当k不为0的时候,都要进入一遍for循环,即使已经将k两两抵消了。其实k > 0只是for循环里面if语句的一个条件,没必要每次都算一遍for循环里面的负数转正数。

134. 加油站

  1. 暴力方法:O(n^2)
  • for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while!
  • 模拟旋转一圈:index = (i + 1) % len(gas)初始化
    • %表示取余
    • 不这样写行不行?——不行,因为index到了数组末尾需要从头开始走,就要用数组长度取余。
  • 暴力会超出时间限制
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
    for i in range(len(gas)):
        rest = gas[i] - cost[i]
        index = (i + 1) % len(gas)
        while(rest > 0 and index != i):
            rest += gas[index] - cost[index]
            index = (index + 1) % len(gas)
        if index == i and rest >= 0:
            return i
    return -1
  1. 贪心写法
  • 定义curSum = gas[i] - cost[i]
  • 贪心思路:从curSum < 0 下标的下一个作为起点。
  • 代码出现的问题:
    • totalSum不会在curSum为负数的时候重置为0,因此是一直累加的。
    • curSum指的是目前累积了多少油量。

一刷先刷到这,动态规划是大头,贪心其实每题都不一样,比较零碎,不容易总结模板出来,先把动态规划总结出套路。

  • 25
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值