贪心算法
没有框架,每题都是局部最优推出全局最优,只要想不出反例,就可以证明可行。
455. 分发饼干
我用了小饼干先满足小胃口的思路。
- 这个思路要for循环遍历饼干,if里面判断饼干能不能满足胃口。因为排序后,已经用最小的饼干去匹配最小的胃口了,如果不能满足,只能换下一个饼干,而不是换下一个胃口。
- 要判断
index<len(g)
,不然超界了。 - 为什么时间复杂度是O(nlogn)?
376.摆动序列
本题也可以用动态规划方法。一刷先不用。
- 思路解析:
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
- 实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)。
- 这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点。
- 在计算是否有峰值的时候,大家知道遍历的下标 i ,计算
prediff(nums[i] - nums[i-1])
和curdiff(nums[i+1] - nums[i])
,如果prediff < 0
&&curdiff > 0
或者prediff > 0
&&curdiff < 0
此时就有波动就需要统计。
- 需考虑3种情况:
- 上下坡中有平坡
- 解决方法:统一采用删左面三个 2 的规则,那么 当
prediff = 0 && curdiff < 0
也要记录一个峰值,因为他是把之前相同的元素都删掉留下的峰值。
所以我们记录峰值的条件应该是:(preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)
- 注意:这里两个坡只记录一个峰值,也就相当于删除了前面多余的2,而不是两个坡都删除掉了。
- 解决方法:统一采用删左面三个 2 的规则,那么 当
- 数组首尾两端
这里我们可以写死,就是 如果只有两个元素,且元素不同,那么结果为 2。
不写死的话,如何和我们的判断规则结合在一起呢?
可以假设,数组最前面还有一个数字,那这个数字应该是什么呢?
之前我们在 讨论 情况一:相同数字连续 的时候, prediff = 0
,curdiff < 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. 最大子数组和
几种解法:
- 暴力解法:
- 将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
- 贪心解法
- 贪心贪的是哪里呢?
如果 -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
- 解题思路:利润是可以分解的。
- 代码随想录里面的解法是对的,不需要考虑两个正数是不是连着,因为连着也正好,在正数出现的前一天买入,在正数出现的最后一天卖出即可。
55. 跳跃游戏
- 解题思路:其实跳几步无所谓,关键在于可跳的覆盖范围!
不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。
这个范围内,别管是怎么跳的,反正一定可以跳过来。
那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!
每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。
贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。 - 代码遇到问题:
cover = max(i + nums[i], cover)
是对的,cover = i + nums[i]
这样是不对的,有些元素0很多的情况会通不过,比如nums=[3,0,8,2,0,0,1]
。
45. 跳跃游戏 II
- 解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!
这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖。 - 精华部分|理解难点:
从下标0的位置记录下覆盖范围是2之后,计算下一步的覆盖范围不是直接记录下标为2位置的,而是记录下标0的覆盖范围内途经的最大覆盖范围,下标2的覆盖范围没有下标1的覆盖范围大,因此用下标1的覆盖范围。用max(i+nums[i], next)
表示。
- 什么时候启动下一步覆盖范围:当前下标的覆盖范围走完了,而且还没走到终点。也就是启动下一步的最大覆盖范围。
- 用的代码随想录方法1.——要搞懂代码每一步怎么运作的,按想法步骤写出来就好了
1005. K 次取反后最大化的数组和
- 贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。
那么如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。
那么又是一个贪心:局部最优:只找数值最小的正整数进行反转,当前数值和可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和 达到最大。 - 那么本题的解题步骤为:
第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
第二步:从前向后遍历,遇到负数将其变为正数,同时K–
第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
第四步:求和 - 时间复杂度: O(nlogn)——为什么?
- 自己写的代码出现的错误:
- 用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. 加油站
- 暴力方法: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
- 贪心写法
- 定义curSum = gas[i] - cost[i]
- 贪心思路:从curSum < 0 下标的下一个作为起点。
- 代码出现的问题:
- totalSum不会在curSum为负数的时候重置为0,因此是一直累加的。
- curSum指的是目前累积了多少油量。
一刷先刷到这,动态规划是大头,贪心其实每题都不一样,比较零碎,不容易总结模板出来,先把动态规划总结出套路。