目录
455.分发饼干
思路
为了满足更多的小孩,就不要造成饼干尺寸的浪费。
大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。
这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。
可以尝试使用贪心策略,先将饼干数组和小孩数组排序。
然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。
如图:
注意事项
注意版本一的代码中,可以看出来,是先遍历的胃口,在遍历的饼干,那么可不可以 先遍历 饼干,在遍历胃口呢?
其实是不可以的。
外面的 for 是里的下标 i 是固定移动的,而 if 里面的下标 index 是符合条件才移动的。
如果 for 控制的是饼干, if 控制胃口,就是出现如下情况 :
if 里的 index 指向 胃口 10, for 里的 i 指向饼干 9,因为 饼干 9 满足不了 胃口 10,所以 i 持续向前移动,而 index 走不到s[index] >= g[i]
的逻辑,所以 index 不会移动,那么当 i 持续向前移动,最后所有的饼干都匹配不上。
所以 一定要 for 控制 胃口,里面的 if 控制饼干
方法一: 大饼干优先
class Solution:
def findContentChildren(self, g, s):
g.sort() # 将孩子的贪心因子排序
s.sort() # 将饼干的尺寸排序
index = len(s) - 1 # 饼干数组的下标,从最后一个饼干开始
result = 0 # 满足孩子的数量
for i in range(len(g)-1, -1, -1): # 遍历胃口,从最后一个孩子开始
if index >= 0 and s[index] >= g[i]: # 遍历饼干
result += 1
index -= 1
return result
方法二:小饼干优先
class Solution:
def findContentChildren(self, g, s):
g.sort() # 将孩子的贪心因子排序
s.sort() # 将饼干的尺寸排序
index = 0
for i in range(len(s)): # 遍历饼干
if index < len(g) and g[index] <= s[i]: # 如果当前孩子的贪心因子小于等于当前饼干尺寸
index += 1 # 满足一个孩子,指向下一个孩子
return index # 返回满足的孩子数目
方法三:栈大饼干优先
from collecion import deque
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
#思路,饼干和孩子按从大到小排序,依次从栈中取出,若满足条件result += 1 否则将饼干栈顶元素重新返回
result = 0
queue_g = deque(sorted(g, reverse = True))
queue_s = deque(sorted(s, reverse = True))
while queue_g and queue_s:
child = queue_g.popleft()
cookies = queue_s.popleft()
if child <= cookies:
result += 1
else:
queue_s.appendleft(cookies)
return result
376. 摆动序列
思路
思路 1(贪心解法)
本题要求通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
相信这么一说吓退不少同学,这要求最大摆动序列又可以修改数组,这得如何修改呢?
来分析一下,要求删除元素使其达到最大摆动序列,应该删除什么元素呢?
用示例二来举例,如图所示:
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
局部最优推出全局最优,并举不出反例,那么试试贪心!
(为方便表述,以下说的峰值都是指局部峰值)
实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)
这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点
在计算是否有峰值的时候,大家知道遍历的下标 i ,计算 prediff=nums[i] - nums[i-1] 和 curdiff=nums[i+1] - nums[i],如果prediff < 0 and curdiff > 0
或者 prediff > 0 and curdiff < 0
此时就有波动就需要统计。
这是我们思考本题的一个大体思路,但本题要考虑三种情况:
- 情况一:上下坡中有平坡
- 情况二:数组首尾两端
- 情况三:单调坡中有平坡
情况一:上下坡中有平坡
例如 [1,2,2,2,1]这样的数组,如图:
它的摇摆序列长度是多少呢? 其实是长度是 3,也就是我们在删除的时候 要不删除左面的三个 2,要不就删除右边的三个 2。
如图,可以统一规则,删除左边的三个 2:
在图中,当 i 指向第一个 2 的时候,prediff > 0 and curdiff = 0
,当 i 指向最后一个 2 的时候 prediff = 0 and curdiff < 0
。
如果我们采用,删左面三个 2 的规则,那么 当 prediff = 0 and curdiff < 0
也要记录一个峰值,因为他是把之前相同的元素都删掉留下的峰值。
所以我们记录峰值的条件应该是: (preDiff <= 0 and curDiff > 0) or (preDiff >= 0 and 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 and preDiff <= 0,那么 result += 1(计算了左面的峰值),最后得到的 result 就是 2(峰值个数为 2 即摆动序列长度为 2)
经上分析,可以写出如下代码:
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
if len(nums) <= 1: return len(nums)
# 记录前一个差值,默认为0,这种为了方便统计数组长度为2且两个数值不想同的情况
prediff = 0
# 记录后一个差值
curdiff = 0
# 计数,默认最右边的元素被视为峰值
count = 1
for i in range(len(nums)-1):
curdiff = nums[i+1] - nums[i]
# 第一种判断写法
if prediff * curdiff <= 0 and curdiff !=0:
# 第二种判断写法
# if (prediff <=0 and curdiff > 0) or (prediff >= 0 and curdiff < 0):
count += 1
prediff = curdiff
return count
此时大家是不是发现 以上代码提交也不能通过本题?
所以此时我们要讨论情况三!
情况三:单调坡度有平坡
在版本一中,我们忽略了一种情况,即 如果在一个单调坡度上有平坡,例如[1,2,2,2,3,4],如图:
图中,我们可以看出,版本一的代码在三个地方记录峰值,但其实结果因为是 2,因为 单调中的平坡 不能算峰值(即摆动)。
之所以版本一会出问题,是因为我们实时更新了 prediff。
那么我们应该什么时候更新 prediff 呢?
我们只需要在 这个坡度 摆动变化的时候,更新 prediff 就行,这样 prediff 在 单调区间有平坡的时候 就不会发生变化,造成我们的误判。
方法一:贪心版本一
class Solution:
def wiggleMaxLength(self, nums):
if len(nums) <= 1:
return len(nums) # 如果数组长度为0或1,则返回数组长度
curDiff = 0 # 当前一对元素的差值
preDiff = 0 # 前一对元素的差值
result = 1 # 记录峰值的个数,初始为1(默认最右边的元素被视为峰值)
for i in range(len(nums) - 1):
curDiff = nums[i + 1] - nums[i] # 计算下一个元素与当前元素的差值
# 如果遇到一个峰值
if (preDiff <= 0 and curDiff > 0) or (preDiff >= 0 and curDiff < 0):
result += 1 # 峰值个数加1
preDiff = curDiff # 注意这里,只在摆动变化的时候更新preDiff
return result # 返回最长摆动子序列的长度
方法二:贪心版本二
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
if len(nums) <= 1:
return len(nums) # 如果数组长度为0或1,则返回数组长度
preDiff,curDiff ,result = 0,0,1 #题目里nums长度大于等于1,当长度为1时,其实到不了for循环里去,所以不用考虑nums长度
for i in range(len(nums) - 1):
curDiff = nums[i + 1] - nums[i]
if curDiff * preDiff <= 0 and curDiff !=0: #差值为0时,不算摆动
result += 1
preDiff = curDiff #如果当前差值和上一个差值为一正一负时,才需要用当前差值替代上一个差值
return result
53. 最大子序和
思路
贪心解法
贪心贪的是哪里呢?
如果 -2 1 在一起,计算起点的时候,一定是从 1 开始计算,因为负数只会拉低总和,这就是贪心贪的地方!
局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
全局最优:选取最大“连续和”
局部最优的情况下,并记录最大的“连续和”,可以推出全局最优。
从代码角度上来讲:遍历 nums,从头开始用 count 累积,如果 count 一旦加上 nums[i]变为负数,那么就应该从 nums[i+1]开始从 0 累积 count 了,因为已经变为负数的 count,只会拖累总和。
这相当于是暴力解法中的不断调整最大子序和区间的起始位置。
那有同学问了,区间终止位置不用调整么? 如何才能得到最大“连续和”呢?
区间的终止位置,其实就是如果 count 取到最大值了,及时记录下来了。例如如下代码:
if count > result: result = count
这样相当于是用 result 记录最大子序和区间和(变相的算是调整了终止位置)。
如动画所示:
红色的起始位置就是贪心每次取 count 为正数的时候,开始一个区间的统计。
常见误区
误区一:
不少同学认为 如果输入用例都是-1,或者 都是负数,这个贪心算法跑出来的结果是 0, 这是又一次证明脑洞模拟不靠谱的经典案例,建议大家把代码运行一下试一试,就知道了,也会理解 为什么 result 要初始化为最小负数了。
误区二:
大家在使用贪心算法求解本题,经常陷入的误区,就是分不清,是遇到 负数就选择起始位置,还是连续和为负选择起始位置。
在动画演示用,大家可以发现, 4,遇到 -1 的时候,我们依然累加了,为什么呢?
因为和为 3,只要连续和还是正数就会 对后面的元素 起到增大总和的作用。 所以只要连续和为正数我们就保留。
这里也会有录友疑惑,那 4 + -1 之后 不就变小了吗? 会不会错过 4 成为最大连续和的可能性?
其实并不会,因为还有一个变量 result 一直在更新 最大的连续和,只要有更大的连续和出现,result 就更新了,那么 result 已经把 4 更新了,后面 连续和变成 3,也不会对最后结果有影响。
方法一:暴力法-提交leetcode会超时
class Solution:
def maxSubArray(self, nums):
result = float('-inf') # 初始化结果为负无穷大
count = 0
for i in range(len(nums)): # 设置起始位置
count = 0
for j in range(i, len(nums)): # 从起始位置i开始遍历寻找最大值
count += nums[j]
result = max(count, result) # 更新最大值
return result
方法二:贪心
class Solution:
def maxSubArray(self, nums):
result = float('-inf') # 初始化结果为负无穷大
count = 0
for i in range(len(nums)):
count += nums[i]
if count > result: # 取区间累计的最大值(相当于不断确定最大子序终止位置)
result = count
if count <= 0: # 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
count = 0
return result