python 贪心算法
贪心算法理论基础
选择每一阶段的局部最优,从而达到全局最优。选择贪心算法的关键在于:可以从局部最优推出整体最优。
验证能不能用贪心:
- 举反例 (想不到反例的时候可以试一下)
- 数学归纳法
贪心算法步骤:
- 将问题分解为若干个子问题
- 找出合适的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
分发饼干
局部最优:大饼干胃给胃口最大的
全局最优:喂饱尽可能多的小孩
# 先喂饱胃口小的
# 从小到大遍历饼干,如果最小的饼干不能喂饱最小的孩子,就找第二小的饼干,以此类推。不能反过来,因为找第二小的孩子的还是不能用最小的饼干满足。
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 < len(g) and s[i] >= g[res]:
res += 1
return res
# 先喂饱胃口大的
# 从大到小遍历孩子,先找最大的孩子看最大的饼干是否满足,如果不满足,第二大的孩子再匹配最大的饼干,以此类推,不能反过来,因为第二大的饼干还是不能满足最大胃口的孩子。
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
g.sort()
s.sort()
res = 0
start = len(s) - 1
for i in range(len(g) - 1, -1, -1):
if start >= 0 and g[i] <= s[start]:
res += 1
start -= 1
return res
摆动序列
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
# 题目里nums长度大于等于1,当长度为1时,其实到不了for循环里去,所以不用考虑nums长度
for i in range(len(nums) - 1):
prediff, curdiff, res = 0, 0, 1
for i in range(len(nums) - 1):
curdiff = nums[i + 1] - nums[i]
# 差值为0时,不算摆动
if curdiff * prediff <= 0 and curdiff != 0:
res += 1
# 如果当前差值和上一个差值为一正一负时,才需要用当前差值替代上一个差值
prediff = curdiff
return res
最大子序列和
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
res = -float('inf')
count = 0
# 一旦和的值小于0,重新开始计算和
for i in range(len(nums)):
count += nums[i]
if count > res:
res = count
if count < 0:
count = 0
return res
买卖股票的最佳时机ii
最终利润可以分解为每天的利润,收集真利润的区间,就是股票买卖的区间。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
# 每天的利润都可以拆解出来
profit = []
sum_profit = 0
for i in range(len(prices) - 1):
# 买入的钱减去卖出的钱
day_profit = prices[i + 1] - prices[i]
profit.append(day_profit)
print(profit)
for day_profit in profit:
if day_profit > 0:
sum_profit += day_profit
return sum_profit
跳跃游戏
局部最优解:每次取最大跳跃步数
整体最优:最后得到整体最大覆盖范围,看是否能到终点
class Solution:
def canJump(self, nums: List[int]) -> bool:
cover = 0
i = 0
if len(nums) == 1: return True
while i <= cover:
cover = max(cover, i + nums[i])
if cover >= len(nums) - 1: return True
i += 1
return False
跳跃游戏ii
局部最优:当前可移动的距离尽可能的多
整体最优:一步尽可能的多,从而达到最小步数
题解:从覆盖范围出发,以最小的步数增加覆盖范围,一旦覆盖范围覆盖到了终点 ,得到的就是最小步数。需统计两个覆盖范围,一个是当前的最大覆盖和下一步的最大覆盖。
class Solution:
def jump(self, nums: List[int]) -> int:
if len(nums) == 1: return 0
jump = 0
next_jump = 0
cur_jump = 0
for i in range(len(nums)):
next_jump = max(i + nums[i], next_jump)
# 这里一开始jump就加了个1,刚好补上最后的一跳,当next_jump >= len(nums) - 1 时,需要再跳一次到最后一个点
if i == cur_jump:
if cur_jump != len(nums) - 1:
jump += 1
cur_jump = next_jump
if next_jump >= len(nums) - 1: break
return jump
K次取反后最大化数组和
局部最优: 让绝对值大的负数变为正数,当前数值达到最大
整体最优:整个数组和达到最大
class Solution:
def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
nums.sort()
i = 0
# 找绝对值最大的负数变为正数,多出来的K用来转换最小的数字
while k > 0 and i < len(nums) - 1 and nums[i] < 0:
nums[i] = -nums[i]
k -= 1
i += 1
# 这里是比较转换后的nums[i-1] 和nums[i]的大小,即正负交界处的最小值
if nums[i] > nums[i - 1]:
i -= 1
while k != 0:
nums[i] = -nums[i]
k -= 1
return sum(nums)
加油站
- 情况一:如果gas的总和小于cost的总和,那么无论从哪里出发,一定是跑不了一圈的
- 情况二:rest[i] = gas[i] - cost[i] 为一天剩下的油, i 从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么起点就是0
- 情况三:如果累加的最小值是负数,骑车就要从非0节点出发,从后向前,看哪个节点能将这个负数填平,能把这个负数填平的节点就是出发节点。
局部最优:当前累加rest[j]的和curSum一旦小于0,起始位置至少要j+1,因为从j开始一定不行
全局最优:找到可以跑一圈的起始位置
# 解法一:
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
if sum(gas) < sum(cost): return -1
min_sum = float('inf')
cur_sum = 0
# 求出累计过程中的最小值
for i in range(len(gas)):
cur_sum += gas[i] - cost[i]
min_sum = min(cur_sum, min_sum)
# 如果累计过程中的最小值都大于0,说明直接从0开始就行
if min_sum >= 0: return 0
# 往前找能填满累计最小值的累计值,直到大于等于0
for i in range(len(gas) - 1, 0 , -1):
min_sum += gas[i] - cost[i]
if min_sum >= 0:
return i
return -1
# 解法二
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
if sum(gas) < sum(cost): return -1
rest = []
cur_sum = 0
start = 0
# 相当于遍历,如果走到i变成负的了,就从i+1开始跑
for i in range(len(gas) - 1):
cur_sum += gas[i] - cost[i]
if cur_sum < 0:
cur_sum = 0
start = i + 1
return start
分发糖果
确定一边之后再确定另一边,例如比较每个孩子的左边,再比较右边,如果两边一起考虑会顾此失彼。
先确定右边评分大于左边的情况(从前向后遍历)
局部最优:只要右边评分比左边大,右边的孩子就多一颗糖果
全局最优:评分最高的右孩子获得比左边孩子更多的糖果
再确定左孩子大于右孩子的情况(从后向前遍历)
确定左孩子大于右孩子一定要从后向前遍历!
贪心策略:
- 从做到右遍历,只比较右边孩子评分比左边大的情况
- 做右到左遍历,只比较左边孩子评分比右边大的情况
# 核心:先比较孩子的左边,再比较孩子的右边
class Solution:
def candy(self, ratings: List[int]) -> int:
n = len(ratings)
candy = [1 for _ in range(n)]
# 从前向后,右孩子和做孩子比较
for i in range(1, n):
if ratings[i] > ratings[i - 1]:
candy[i] = candy[i - 1] + 1
# 从后向前,左孩子和右孩子比较
for i in range(n - 2, -1, -1):
if ratings[i] > ratings[i + 1]:
# 可能第一轮比较下来左孩子的分数已经很高了
candy[i] = max(candy[i], candy[i + 1] + 1)
return sum(candy)
柠檬水找零
局部最优:遇到账单20,优先消耗10美元,完成本次找零
全局最优:完成全部账单的找零
class Solution:
def lemonadeChange(self, bills: List[int]) -> bool:
bill_dict = {5: 0, 10: 0, 20: 0}
for bill in bills:
if bill == 5:
bill_dict[5] += 1
elif bill == 10:
bill_dict[10] += 1
bill_dict[5] -= 1
else :
bill_dict[20] += 1
if bill_dict[10] > 0:
bill_dict[10] -= 1
bill_dict[5] -= 1
else:
bill_dict[5] -= 3
if bill_dict[5] < 0:
return False
return True
根据身高重建队列
先根据身高排序,然后根据K插入
**局部最优:**优先按身高高的people的k来插入,插入后的people满足队列属性
全局最优: 最后都做完插入操作,整个队列满足题目队列属性
class Solution:
def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
# 先按照h维度的身高顺序从高到低排序。确定第一个维度
# lambda返回的是一个元组:当-x[0](维度h)相同时,再根据x[1](维度k)从小到大排序
people.sort(key = lambda x:(-x[0], x[1]))
que = []
# 根据每个元素的第二个维度k,贪心算法,进行插入
# people已经排序过了:同一高度时k值小的排前面。
for p in people:
que.insert(p[1], p)
return que
用最少数量的箭引爆气球
局部最优:一箭能射最多
全局最优:弓箭最少
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
if len(points) == 0: return 0
points.sort(key=lambda x: x[0])
min_arrow = 1
for i in range(1, len(points)):
if points[i - 1][1] < points[i][0]: # 气球i和气球i-1不挨着,注意这里不是>=
min_arrow += 1
else:
# 更新重叠气球最小右边界,上面的右边界有可能比下面的大,会错过下面的气球
points[i][1] = min(points[i][1], points[i - 1][1])
return min_arrow
无重叠区间
- 按照右边界排序,就要从左向右遍历,因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以从左向右遍历,优先选右边界小的
- 按照左边界排序,就要从右向左遍历,因为左边界数值越大越好(越靠右),这样就给前一个区间的空间就越大,所以可以从右向左遍历。
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
intervals.sort(key = lambda x: x[1])
count = 0 # 记录重叠的个数
end = intervals[0][1]
for i in range(len(intervals)):
# 注意这里的 <= 数组的最后和end一样一定是也是重叠的
if i != 0 and intervals[i][0] < end <= intervals[i][1]:
count += 1
continue
end = intervals[i][1]
return count
划分字母区间
题解:本题相当于找之前遍历过的所有字母的最远边界,说明这个边界就是分割点了
class Solution:
def partitionLabels(self, s: str) -> List[int]:
hash_map = [0] * 26
# 用hash_map记录每个字符的最远距离
for i in range(len(s)):
hash_map[ord(s[i]) - ord('a')] = i
res = []
left = 0
right = 0
for i in range(len(s)):
right = max(right, hash_map[ord(s[i]) - ord('a')])
if i == right:
res.append(right - left + 1)
left = right + 1
return res
合并区间
题解:
局部最优:先按左边界排序,每次合并都取最大的右边界,这样就可以合并更多的区间了
全局最优:合并所有重叠的区间
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
if not intervals: return []
intervals.sort(key = lambda x: x[0])
res = []
left = intervals[0][0]
right = intervals[0][1]
for i in range(len(intervals)):
# 只要比左边大就能合并
if right >= intervals[i][0]:
right = max(right, intervals[i][1])
else:
res.append([left, right])
left = intervals[i][0]
right = intervals[i][1]
# 把最后一组合并进去
res.append([left, right])
return res
单调递增的数字
局部最优:遇到strNum[i -1] > strNum[i]的情况, 让strNum[i -1] --, 然后strNum[i]给为9,可以保证这两位变成最大单调递增整数。
全局最优:得到小于等于N的最大单调递增的整数。
class Solution:
def monotoneIncreasingDigits(self, n: int) -> int:
a = list(str(n))
for i in range(len(a)- 1, 0, -1):
if int(a[i]) < int(a[i - 1]):
a[i - 1] = str(int(a[i - 1]) - 1)
a[i:] = '9' * (len(a) - i)
return int(''.join(a))
买卖股票的最佳时机含手续费
- 买入日期:其实很好想,遇到更低点就记录一下。
- 卖出日期:这个就不好算了,但也没有必要算出准确的卖出日期,只要当前价格大于(最低价格+手续费),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天(并不需要计算是具体哪一天)。
三种情况
- 情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润
- 情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格了。
- 情况三:不作操作,保持原有状态(买入,卖出,不买不卖)
class Solution:
def maxProfit(self, prices: List[int], fee: int) -> int:
res = 0
min_price = prices[0]
for i in range(1, len(prices)):
if prices[i] < min_price:
min_price = prices[i]
elif prices[i] >= min_price and prices[i] <= min_price + fee:
continue
else:
res += prices[i] - min_price - fee
min_price = prices[i] - fee
return res
监控二叉树
局部最优:让子节点的父节点按摄像头,所用摄像头最少
整体最优:全部摄像头数量最少
三种状态:
- 0:改节点无覆盖
- 1: 本节点有摄像头
- 2:本节点有覆盖
单层逻辑:
- 情况1:左右节点都有覆盖
- 情况2:左右节点至少有一个无覆盖的情况
- 情况3:左右节点至少有一个有摄像头
- 情况4:头结点没有覆盖
class Solution:
def minCameraCover(self, root: TreeNode) -> int:
# 从下往上安装摄像头:跳过leaves这样安装数量最少,局部最优 -> 全局最优
# 先给leaves的父节点安装,然后每隔两层节点安装一个摄像头,直到Head
# 0: 该节点未覆盖
# 1: 该节点有摄像头
# 2: 该节点有覆盖
self.result = 0
if self.travelsal(root) == 0:
self.result += 1
return self.result
def travelsal(self, cur):
if not cur: return 2
# 从下往上遍历:后序(左右中)
left = self.travelsal(cur.left)
right = self.travelsal(cur.right)
# Case 1:
# 左右节点都有覆盖
if left == 2 and right == 2:
return 0
# Case 2:
# left == 0 && right == 0 左右节点无覆盖
# left == 1 && right == 0 左节点有摄像头,右节点无覆盖
# left == 0 && right == 1 左节点有无覆盖,右节点摄像头
# left == 0 && right == 2 左节点无覆盖,右节点覆盖
# left == 2 && right == 0 左节点覆盖,右节点无覆盖
elif left == 0 or right == 0:
self.result += 1
return 1
# Case 3:
# left == 1 && right == 2 左节点有摄像头,右节点有覆盖
# left == 2 && right == 1 左节点有覆盖,右节点有摄像头
# left == 1 && right == 1 左右节点都有摄像头
elif left == 1 or right == 1:
return 2