LeetCode 101:和你一起你轻松刷题(python版)
注:作者:高畅 Chang Gao,原书为c++版本,解题思路清晰,知识点全面,是一本好书;翻译成python版本的解法可能未必是最优解法,由于本人是新手小白,算法实现是第一步,优化后面再弄。如有侵权,联系删除
第 2 章 最易懂的贪心算法
2.1 算法解释
顾名思义, 贪心算法或贪心思想采用贪心的策略,保证每次操作都是局部最优的,从而使最后得到的结果是全局最优的。
举一个最简单的例子:小明和小王喜欢吃苹果,小明可以吃五个,小王可以吃三个。已知苹果园里有吃不完的苹果,求小明和小王一共最多吃多少个苹果。在这个例子中,我们可以选用的贪心策略为,每个人吃自己能吃的最多数量的苹果,这在每个人身上都是局部最优的。又因为全局结果是局部结果的简单求和,且局部结果互不相干,因此局部最优的策略也同样是全局最优的策略。
2.2 分配问题
455. Assign Cookies (Easy)
题目描述
有一群孩子和一堆饼干,每个孩子有一个饥饿度,每个饼干都有一个大小。每个孩子只能吃最多一个饼干,且只有饼干的大小大于孩子的饥饿度时,这个孩子才能吃饱。求解最多有多少孩子可以吃饱。
输入输出样例
输入两个数组,分别代表孩子的饥饿度和饼干的大小。输出最多有多少孩子可以吃饱的数量
input: [1,2], [1,2,3]
output: 2
在这个样例中,我们可以给两个孩子喂 [1,2]、 [1,3]、 [2,3] 这三种组合的任意一种,两个孩子都可以吃饱
题解
因为饥饿度最小的孩子最容易吃饱,所以我们先考虑这个孩子。为了尽量使得剩下的饼干可 以满足饥饿度更大的孩子,所以我们应该把大于等于这个孩子饥饿度的、且大小最小的饼干给这 个孩子。满足了这个孩子之后,我们采取同样的策略,考虑剩下孩子里饥饿度最小的孩子,直到没有满足条件的饼干存在。
简而言之,这里的贪心策略是,给剩余孩子里最小饥饿度的孩子分配最小的能饱腹的饼干。
至于具体实现,因为我们需要获得大小关系,一个便捷的方法就是把孩子和饼干分别排序。这样我们就可以从饥饿度最小的孩子和大小最小的饼干出发,计算有多少个对子可以满足条件。
"""
对两组数据排序,如果胃口最小的饼干满足最小胃口的孩子,则分配成功,不满足,则换大尺寸饼干
"""
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
g.sort()
s.sort()
num = min(len(g),len(s))
i, j = 0, 0
while i < len(g) and j < len(s):
if g[i] <= s[j]:
i += 1
j +=1
else:
j += 1
return i
135.Candy(Hard)
题目描述
一群孩子站成一排,每一个孩子有自己的评分。现在需要给这些孩子发糖果,规则是如果一个孩子的评分比自己身旁的一个孩子要高,那么这个孩子就必须得到比身旁孩子更多的糖果;所有孩子至少要有一个糖果。求解最少需要多少个糖果。
输入输出样例
输入是一个数组,表示孩子的评分。输出是最少糖果的数量。
input: [1,0,2]
output: 5
在这个样例中,最少的糖果分法是 [2,1,2]。
题解
做完了题目 455,你会不会认为存在比较关系的贪心策略一定需要序或是选择?虽然这一道题也是运用贪心策略,但我们只需要简单的两次遍历即可:把所有孩子的糖果数初始化为 1;
先从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的糖果数加 1;再从右往左遍历一遍,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加 1。
通过这两次遍历,分配的糖果就可以满足题目要求了。这里的贪心策略即为,在每次遍历中,只考虑并更新相邻一侧的大小关系。在样例中,我们初始化糖果分配为 [1,1,1],第一次遍历更新后的结果为[1,1,2],第二次遍历更新后的结果为 [2,1,2]。
"""
对于糖果的分发,孩子的位置已经固定,每个孩子得分也固定
从头遍历,遇到分数高的奖励前一名糖果数+1,
反向遍历,遇到分数高的奖励后一名的糖果数+1(应注意,遍历过程中若本身糖果数已经高于后一位则不需要奖励)
"""
class Solution:
def candy(self, ratings: List[int]) -> int:
nums = [1] * len(ratings)
for i in range(1, len(ratings)):
if ratings[i] > ratings[i - 1]:
nums[i] = nums[i - 1] + 1
for i in range(len(ratings) - 1, 0, -1):
if ratings[i] < ratings[i - 1]:
if nums[i - 1] <= nums[i]:
nums[i - 1] = nums[i] + 1
result = sum(nums)
return result
2.3 区间问题
435. Non-overlapping Intervals (Medium)
题目描述
给定多个区间,计算让这些区间互不重叠所需要移除区间的最少个数。起止相连不算重叠。
输入输出样例
输入是一个数组,数组由多个长度固定为 2 的数组组成,表示区间的开始和结尾。输出一个整数,表示需要移除的区间数量。
input: [[1,2], [2,4], [1,3]]
output: 1
在这个样例中,我们可以移除区间 [1,3],使得剩余的区间 [[1,2], [2,4]] 互不重叠。
题解
在选择要保留区间时,区间的结尾十分重要:选择的区间结尾越小,余留给其它区间的空间就越大,就越能保留更多的区间。因此,我们采取的贪心策略为,优先保留结尾小且不相交的区间。
具体实现方法为,先把区间按照结尾的大小进行增序排序,每次选择结尾最小且和前一个选择的区间不重叠的区间。
在样例中,排序后的数组为 [[1,2], [1,3], [2,4]]。按照我们的贪心策略,首先初始化为区间[1,2];由于 [1,3] 与 [1,2] 相交,我们跳过该区间;由于 [2,4] 与 [1,2] 不相交,我们将其保留。因此最终保留的区间为 [[1,2], [2,4]]
注:排序可以根据实际情况判断区间按开头排序还是结尾排序,或开头与结尾单独排序
"""
如果判定重叠,需要移除一个,判定区间最小的,移除较大的一个才能够最大的实现无重叠区域
"""
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
intervals.sort()
j = 0
for i in range(len(intervals)-1):
if intervals[i+1][0] >= intervals[i][0] and intervals[i+1][0] < intervals[i][1]:
if intervals[i][1] <= intervals[i+1][1]:
intervals[i], intervals[i+1] = intervals[i+1],intervals[i]
j += 1
return j
2.4 练习
基础难度
605. Can Place Flowers (Easy)
采取什么样的贪心策略,可以种植最多的花朵呢?
452 Minimum Number of Arrows to Burst Balloons (Medium)
这道题和题目 435 十分类似,但是稍有不同,具体是哪里不同呢?
763 Partition Labels (Medium)
为了满足你的贪心策略,是否需要一些预处理?
注意 在处理数组前,统计一遍信息(如频率、个数、第一次出现位置、最后一次出现位置等)可以使题目难度大幅降低。
122 Best Time to Buy and Sell Stock II (Easy)
股票交易题型里比较简单的题目,在不限制交易次数的情况下,怎样可以获得最大利润呢?
进阶难度
406 Queue Reconstruction by Height (Medium)
温馨提示,这道题可能同时需要排序和插入操作。
"""
605 种花问题
本题涉及到最优种花问题,使用贪心算法,每(0,0,0)可以种植一棵(边界除外)
解题技巧,在边界前和后再填充一个数,这样可以解决所有边界出现的特殊情况(如只有一个值等)
"""
class Solution:
def canPlaceFlowers(self, flowerbed: List[int], n: int) -> bool:
flowerbed = [0] + flowerbed + [0]
for i in range(1, len(flowerbed)-1):
if [flowerbed[i-1],flowerbed[i],flowerbed[i+1]] == [0,0,0]:
n -= 1
flowerbed[i] = 1
if n > 0:
return False
else:
return True
"""
452 用最少数量的箭引爆气球
本题涉及到最优射击问题,用最少的钉射中最多的气球,就是找到最大的相交区间
解题技巧:根据数列尾部排序,只要前区间大于最前面的后区间就判定+1,否则必定有相同区间
"""
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
if not points:
return 0
points.sort(key=lambda x:x[1])
j = 1
first_end = points[0][1]
for i in range(1, len(points)):
if points[i][0] > first_end:
j += 1
first_end = points[i][1]
return j
"""
763 划分字母区间
本题涉及到最大划分区域,找到每个字母形成的区域,并确定最大并集
使用循环找到每个字母形成的最大区间
解题技巧:找到字母出现的最后位置(使用了enumerate()函数)
使用for循环找到每个字母形成的最大范围(head与tail)
"""
class Solution(object):
def partitionLabels(self, S):
last = {value: key for key, value in enumerate(S)}#每一个字符串出现的最后位置
#print(last)#{'a': 8, 'b': 5, 'c': 7, 'd': 14}
head = tail = 0
result = [] # 结果列表
for key, value in enumerate(S):
tail = max(tail, last[value])
if key == tail:
result.append(tail - head + 1)
head = tail + 1
return result
"""
122 买卖股票的最佳时机 II
解题思路:由于只能买和卖只能交替进行,故最低买最高卖并不能得到最优解
解题技巧:题目并没有规定同一天只可以交易一次,因此在交易过程中,贪心算法只要后一天高于当天,即可当天买入第二天卖出,同理可以第二天再买入
"""
class Solution:
def maxProfit(self, prices: List[int]) -> int:
profit = 0
for i in range(1, len(prices)):
if prices[i] > prices[i-1]:
profit += prices[i] - prices[i-1]
return profit
"""
406 根据身高重建队列
本题为排序题,需要找到自己的位置,位置由身高h 和 前面身高不低于自身的k值确定
解题思路:K值大于0移动相应位置,首先针对身高进行排序,使用people.sort(key=lambda x:(-x[0],x[1]))方法将h和K相反的排序方式,身高采用倒序更容易实现(遍历原列表,在新列表中根据K值排序)
"""
class Solution:
def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
if not people:
return []
people.sort(key=lambda x:(-x[0],x[1]))
new_people = []
for p in people:#將所有人插入正确的位置
new_people.insert(p[1],p)
return new_people