这篇博客不会涵盖 LeetCode 所有贪心算法题目,而是根据网上相关推荐筛选出比较经典的贪心算法题目,按照由易到难的顺序介绍。
1、分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i i i ,都有一个胃口值 g i g_i gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j j j ,都有一个尺寸 s j s_j sj 。如果 s j ≥ g i s_j \ge g_i sj≥gi ,我们可以将这个饼干 j j j 分配给孩子 i i i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
注意:
你可以假设胃口值为正。
一个小朋友最多只能拥有一块饼干。
分析
要求: 满足最多的孩子
约束条件: 饼干数量;饼干尺寸;孩子胃口;每个孩子至多一块饼干
为了尽可能满足更多的孩子,因此考虑优先满足胃口小的孩子,同一个孩子可能有多个饼干可以满足他,对于胃口较小的孩子,我们选择将能够满足他的最小的饼干分给他,这样其他较大的饼干可能可以用来满足胃口更大的孩子。从而有希望满足最多的孩子。
根据以上分析,我们考虑代码,优先满足胃口较小的孩子,需要根据孩子胃口从小到大排序。胃口较小的孩子选择满足他的最小的饼干,需要对饼干按从小到大排序。 P y t h o n 3 Python3 Python3 代码具体如下:
class Solution:
def findContentChildren(self, g, s):
"""
:type g: List[int]
:type s: List[int]
:rtype: int
"""
g.sort(); s.sort()
count, i, j = 0, 0, 0
while i < len(g) and j < len(s):
if s[j] >= g[i]: # 找到满足孩子胃口的最小的饼干
count += 1
i += 1; j += 1 # 开始查找能够满足下一个孩子的饼干
else:
j += 1
return count
总结:
此类问题类似于双方博弈,要求一方胜利最多的策略,比如:田忌赛马问题。此类题目通常具有如下特点:
- 包含博弈双方,求最优策略
- 满足 1 v s 1 1~vs~1 1 vs 1 ,不能一对多,比如每匹马只能比一场,每块饼干只能给一个孩子
2、摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [ 1 , 7 , 4 , 9 , 2 , 5 ] [1,7,4,9,2,5] [1,7,4,9,2,5] 是一个摆动序列,因为差值 ( 6 , − 3 , 5 , − 7 , 3 ) (6,-3,5,-7,3) (6,−3,5,−7,3) 是正负交替出现的。相反, [ 1 , 4 , 7 , 2 , 5 ] [1,4,7,2,5] [1,4,7,2,5] 和 [ 1 , 7 , 4 , 5 , 5 ] [1,7,4,5,5] [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
分析
要求: 最长摆动子序列
约束条件: 连续数字之间的差严格地在正数和负数之间交替
根据约束条件可知,摆动序列中,除了边缘两个值以外,中间所有值必须满足下列条件之一:
- 大于左右相邻两个值
- 小于左右相邻两个值
即:中间所有值为序列的极值点
取下图中所有红色点可以构成最长摆动序列
P
y
t
h
o
n
3
Python3
Python3 代码如下:
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
n = len(nums)
if n < 2:
return n
count, flag = 1, 0
for i in range(1, n):
if nums[i] > nums[i-1] and flag != 1:
count += 1
flag = 1
elif nums[i] < nums[i-1] and flag != -1:
count += 1
flag = -1
return count
3、移除 K 个数字
给定一个以字符串表示的非负整数 n u m num num,移除这个数中的 k k k 位数字,使得剩下的数字最小。
注意:
n
u
m
num
num 的长度小于
10002
10002
10002 且
≥
k
≥ k
≥k。
n
u
m
num
num 不会包含任何前导零。
分析
要求: 移除 K 位后剩下数字最小
注意: 如果移除 K 位后剩余数字的开头为
′
0
′
'0'
′0′ 则要将它们去除。
由于要求剩下数字最小,优先考虑移除高位较大的数字。逆向思考这个问题,移除 K 个数字等价于寻找 n-K 个数字组成的子序列最小。即,每一位找到该位置可能的最小。举例如下:
num = ‘125413’
K = 3
通过分析发现,去除 3 位后剩余 3 位,要寻找每一位可能的最小(优先满足高位最小)
第一位:由于可以删除 K 个,所以第一位可以选择前 4 位里最小的数为 1(位置0)K 仍然为 3
第二位:从位置 1 开始(因为上一个数选了位置 0),最多还可以删除 K 个,选择 1—4 位中最小的数,即 2(位置 1)K 仍然为 3
第三位:从位置 2 开始,最多删除 K 个,选择 2—5 位中最小的数 1 (位置 4)由于删除了 1 前面的 5 和 4 ,所以 K 修正为 1。
最后:去除后面的 K 位,最后得到 ‘121’,删除了‘543’
通过以上分析不难发现,这个题目和 滑动窗口的最大值
有异曲同工之妙,区别在于这里的窗口大小 K 是变化的,而且窗口不是逐个滑动的,而是根据上一个窗口最优解的位置来确定下一窗口位置,下一窗口的大小由之前删除的元素数量和初始 K 值共同决定。
因此,我们使用栈来实现这一算法。
- 将数字从头开始逐个放入栈中。如果当前值比栈顶元素小,并且 K > 0(说明还可以删除)
- 栈顶元素出栈(删除),修正 K 值,直到栈空,或者 K 为 0,或者当前值不大于栈顶元素为止
- 若以上过程结束后 K 仍然大于 0,则将栈顶的 K 个元素出栈。
- 验证栈底元素是否为 ‘0’,去除开始所有的 ‘0’。
class Solution:
def removeKdigits(self, num: str, k: int) -> str:
stack = []
n = len(num)
if n <= k:
stack.append('0')
else:
for i in range(n):
# 栈顶数字大于当前数字 k>0 出栈
while stack and k > 0 and stack[-1] > num[i]:
stack.pop()
k -= 1
stack.append(num[i])
if k > 0:
stack = stack[:-k]
while stack and stack[0] == "0": # 移除0
stack.pop(0)
res = ''.join(stack)
return res if res else '0'
4、跳跃游戏
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
思路:
- 从第 i i i 个位置,最远可以跳到 n u m s [ i ] + i nums[i]+i nums[i]+i
- 逐个位置遍历,始终记录当前所能到达的最远位置 r e a c h reach reach
- 当前位置超过所能到达的最远位置时,返回 F a l s e False False
- 所能到达的最远位置 r e a c h reach reach 超过最右边位置时,返回 T r u e True True
代码
class Solution:
def canJump(self, nums: List[int]) -> bool:
reach = 0
n = len(nums)
for i in range(n):
if i > reach or reach > n :
break
if i + nums[i] > reach:
reach = i + nums[i]
return reach >= n - 1
5、跳跃游戏 Ⅱ
class Solution:
def jump(self, nums: List[int]) -> int:
n = len(nums)
if n == 1:
return 0
reach = 0
next_reach = nums[0]
step = 0
for i in range(n):
next_reach = max(i + nums[i], next_reach)
if next_reach >= n - 1:
return step + 1
if i == reach:
step += 1
reach = next_reach
return step
6、射击气球
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以 y y y 坐标并不重要,因此只要知道开始和结束的 x x x 坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在 1 0 4 10^4 104 个气球。
一支弓箭可以沿着 x x x 轴从不同点完全垂直地射出。在坐标 x x x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 x s t a r t , x e n d xstart,xend xstart,xend, 且满足 x s t a r t ≤ x ≤ x e n d xstart ≤ x ≤ xend xstart≤x≤xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
思路:
- 按气球区间的右端点进行从小到大排序,取当前右端点为射穿第一个气球的箭所能到达的最右的位置。
- 依次遍历每个气球的区间,若当前气球的左端点在上一气球的右端点之前(两个气球有重叠),则不需要新的箭,否则需要新的箭射穿当前气球。
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
n = len(points)
if n < 2:
return n
# 按照区间的末尾端点排序
points.sort(key = lambda x: x[1])
res = 1
# 最远距离:使用当前这只箭能引爆气球的最远距离
reach = points[0][1]
for i in range(1, n):
if points[i][0] > reach:
reach = points[i][1]
res += 1
return res
7、最优加油方法
在一条环路上有 N N N 个加油站,其中第 i i i 个加油站有汽油 g a s [ i ] gas[i] gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i i i 个加油站开往第 i + 1 i+1 i+1 个加油站需要消耗汽油 c o s t [ i ] cost[i] cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 − 1 -1 −1。
说明:
- 如果题目有解,该答案即为唯一答案。
- 输入数组均为非空数组,且长度相同。
- 输入数组中的元素均为非负数。
思路 1:
检查每个加油站:
- 以该加油站为起点
- 模拟环路行驶,若到达每一站的油量 ≥ 0 \ge0 ≥0 则可以环路行驶。
该方法时间复杂度为 O ( n 2 ) O(n^2) O(n2)
思路 2:
-
初始化 t o t a l _ t a n k total\_tank total_tank 和 c u r r _ t a n k curr\_tank curr_tank 为 0 0 0 ,并且选择 0 0 0 号加油站为起点。
-
遍历所有的加油站:
-
每一步中,都通过加上 g a s [ i ] gas[i] gas[i] 和减去 c o s t [ i ] cost[i] cost[i] 来更新 t o t a l t a n k total_tank totaltank 和 c u r r _ t a n k curr\_tank curr_tank 。
-
如果在 i + 1 i + 1 i+1 号加油站, c u r r t a n k < 0 curr_tank < 0 currtank<0 ,将 i + 1 i + 1 i+1 号加油站作为新的起点,同时重置 c u r r _ t a n k = 0 curr\_tank = 0 curr_tank=0 ,让油箱也清空。
-
-
如果 t o t a l _ t a n k < 0 total\_tank < 0 total_tank<0 ,返回 − 1 -1 −1 ,否则返回 s t a r t i n g s t a t i o n starting~station starting station。
时间复杂度为 O ( n ) O(n) O(n)
class Solution:
def canCompleteCircuit(self, gas, cost):
"""
:type gas: List[int]
:type cost: List[int]
:rtype: int
"""
n = len(gas)
total_tank, curr_tank = 0, 0
start = 0
for i in range(n):
total_tank += gas[i] - cost[i]
curr_tank += gas[i] - cost[i]
# If one couldn't get here
if curr_tank < 0:
# Pick up the next station as the starting one.
start = i + 1
# Start with an empty tank.
curr_tank = 0
return start if total_tank >= 0 else -1