什么是贪心
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿?指定每次拿最大的,最终结果就是拿走最大数额的钱。每次拿最大的就是局部最优,最后拿走最大数额的钱就是推出全局最优。
什么时候用贪心
贪心算法唯一的难点就是如何通过局部最优,推出整体最优。如果手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。如果不可行,可能需要动态规划。
贪心算法一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
序列问题
LeetCode376题 摆动序列
序列中峰值组成的序列,即为摆动序列的最长子序列,需要注意平坡的情况(包括单调平坡、峰值平坡)
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
if len(nums) <= 1:
return len(nums)
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
preDiff = curDiff # 只在摆动变化的时候更新preDiff
return result
LeetCode738题 单调自增的数字
若出现递减的情况,需要让递减开始位置数字减1,后面所有位置补9;其他情况直接取对应位置数字即可。正序遍历解法:
class Solution:
def monotoneIncreasingDigits(self, n: int) -> int:
res = ''
s = str(n)
# pre代表最后一个处理完结果的位置
pre = -1
for i in range(len(s) - 1):
# 递增将对应数字直接写入结果;相等直接跳过;递减直接-1补9 跳出循环
if int(s[i]) < int(s[i + 1]):
res += s[pre + 1: i + 1]
pre = i
elif int(s[i]) > int(s[i + 1]):
# 如果-1位置是1,则不补0,直接补9,补0会导致数字不合法
if s[pre + 1] == '1':
res += (len(s) - pre - 2) * '9'
else:
res += str(int(s[pre + 1]) - 1) + (len(s) - pre - 2) * '9'
pre = len(s) - 1
break
# 补充处理未处理的数字
if pre < len(s) - 1:
res += s[pre + 1:]
return int(res)
LeetCode122题 买卖股票最佳时机II
只要遇到递增序列,就进行收益计算
class Solution:
def maxProfit(self, prices: List[int]) -> int:
res = 0
for i in range(1, len(prices)):
# 只要遇到递增序列,就进行收益计算
if prices[i] >= prices[i - 1]:
res += prices[i] - prices[i - 1]
return res
LeetCode53题 最大子序和
用一个变量记录连续和,如果和为正,则持续相加;否则直接更新,中间记录最大的连续和
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
res = float('-inf')
cur_sum = 0
for i in range(len(nums)):
if cur_sum > 0:
cur_sum += nums[i]
else:
cur_sum = nums[i]
res = max(res, cur_sum)
return res
LeetCode134题 加油站
假设1:如果total_sum>=0,那么一定可以跑完一圈
假设2:当前累加和cur_sum一旦小于0,出发位置至少要是i+1,从i之前开始一定不行
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
res = 0
total_sum = 0
cur_sum = 0
for i in range(len(gas)):
cur_sum += gas[i] - cost[i]
total_sum += gas[i] - cost[i]
if cur_sum < 0:
res = i + 1
cur_sum = 0
if total_sum < 0:
return -1
return res
LeetCode1005题 K次取反后最大化的数组和
可对数组排序(也可对数组绝对值排序),然后按顺序处理
class Solution:
def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
# 四种情形:都是正;都是负;有正有负,负数量>=k;都是负;有正有负,负数量<k
nums.sort()
# 为了方便处理都是负数情况
nums.append(float('inf'))
res = 0
for i in range(len(nums) - 1):
# 找到正负数分隔点
if nums[i] < 0 and nums[i + 1] >= 0:
if k == 0:
res += nums[i]
elif k % 2 == 1:
res -= nums[i]
elif - nums[i] <= nums[i + 1]:
res += nums[i]
elif - nums[i] > nums[i + 1]:
res -= nums[i] + 2 * nums[i + 1]
# 处理负数
elif nums[i] < 0 and nums[i + 1] < 0:
if k > 0:
k -= 1
res -= nums[i]
else:
res += nums[i]
# 处理都是正数情况
elif i == 0 and nums[i] >= 0 and k % 2 == 1:
res -= nums[i]
# 处理正数
else:
res += nums[i]
return res
两个维度权衡问题
LeetCode135题 分发糖果
从左右两个方向分别计算需要的糖果数,然后取两个方向需要的最大糖果数
class Solution:
def candy(self, ratings: List[int]) -> int:
res = 0
c = [1] * len(ratings)
for i in range(1, len(ratings)):
if ratings[i] > ratings[i - 1]:
c[i] = c[i - 1] + 1
c_reverse = [1] * len(ratings)
for i in range(len(ratings) - 2, -1, -1):
if ratings[i] > ratings[i + 1]:
c_reverse[i] = c_reverse[i + 1] + 1
for i in range(len(ratings)):
res += max(c[i], c_reverse[i])
return res
LeetCode406题 根据身高重建队列
按照身高降序排序、按照人数升序排序,这样排序后,后面的元素不会影响前面的元素,就可以通过插入后面元素,来重建队列数组
class Solution:
def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
res = []
people.sort(key=lambda x: (- x[0], x[1]))
for p in people:
k = p[1]
# 插入第k个位置,以满足人数要求
res = res[:k] + [p] + res[k:]
return res
区间问题
LeetCode55题 跳跃游戏
class Solution:
def canJump(self, nums: List[int]) -> bool:
if len(nums) == 1:
return True
# cover代表可以覆盖的最大范围
cover = 0
for i in range(len(nums) - 1):
# 确保可以跳跃到这个位置
if i <= cover:
# 更新覆盖范围
cover = max(cover, i + nums[i])
# 如果已经覆盖到终点,就返回
if cover >= len(nums) - 1:
return True
return False
LeetCode45题 跳跃游戏II
class Solution:
def jump(self, nums: List[int]) -> int:
if len(nums) == 1:
return 0
res = 0
# 记录当前跳跃的最大范围
cover = 0
# 记录下次跳跃最大的范围
next_cover = 0
for i in range(len(nums)):
next_cover = max(next_cover, i + nums[i])
# 当循环到当前跳跃的最大范围时,还没有跳到终点,需要再进行跳跃
if i == cover:
cover = next_cover
res += 1
# 判断跳跃后,是否能跳到终点
if cover >= len(nums) - 1:
return res
LeetCode452题 用最少数量的箭引爆气球
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
res = 1
# 按左区间排序后,判断相邻区间是否重合只需用 i-1的右区间 vs i的左区间
points.sort(key=lambda x: x[0])
for i in range(1, len(points)):
# 当前区间和之前区间不重合,则需要增加弓箭
if points[i - 1][1] < points[i][0]:
res += 1
# 当前区间和之前区间重合,则需要更新区间右边界,以判断和之后区间是否重合
else:
points[i][1] = min(points[i][1], points[i - 1][1])
return res
LeetCode56题 合并区间
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort(key=lambda x: x[0])
res = [intervals[0]]
# 记录当前重叠区间,最大右边界
cur_max_right = intervals[0][1]
for i in range(1, len(intervals)):
if cur_max_right < intervals[i][0]:
res.append(intervals[i])
cur_max_right = intervals[i][1]
else:
if cur_max_right < intervals[i][1]:
res[-1][1] = intervals[i][1]
cur_max_right = intervals[i][1]
return res
贪心和二叉树的结合
LeetCode968题 监督二叉树
贪心思路:尽量在叶子结点的父节点放摄像头,然后尽量每隔两个节点放个摄像头
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def minCameraCover(self, root: Optional[TreeNode]) -> int:
self.res = 0
# 若根节点没被监控,还需要安装一个摄像头
if self.traversal(root) == 0:
self.res += 1
return self.res
def traversal(self, root):
# 返回0代表没被监控;1表示被监控;2表示安装摄像头
if root is None:
return 1
left = self.traversal(root.left)
right = self.traversal(root.right)
# 后续遍历
# 若左右孩子被监控,则间隔两个节点放摄像头,节点返回没被监控
if left == 1 and right == 1:
return 0
# 若左右孩子至少一个没被监控,则节点必须安装摄像头,结果+1
elif left == 0 or right == 0:
self.res += 1
return 2
# 若左右孩子至少一个安装摄像头,则节点已被监控
elif left == 2 or right == 2:
return 1