贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
钞票支付问题
设有无穷张面值为1元、5元、10元、20元、100元、200元的钞票,现要使用这些钞票支付 x x x 元,最少需要几张钞票。
思路:
贪心策略:观察现有钞票的面额发现,任意面额是比自己小的面额的倍数关系,所以当使用一张较大面额钞票时,若用较小面额钞票替换,一定需要更多的其他面额钞票,因此贪心策略就是首先要使用尽可能多的面额大的钞票。
def change(money,x):
n=len(money)
money.sort(reverse=True)
ans=0
for i in range(n):
use=x//money[i]
ans+=use
x-=money[i]*use
print('需要面额为{}的钞票{}张'.format(money[i],use))
return ans
print(change([200,100,20,10,5,1],628))
455.分发饼干【简单】
LeetCode传送门
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
注意:你可以假设胃口值为正。一个小朋友最多只能拥有一块饼干。
思路: 贪心+双指针
- 某个饼干如果不能满足某个孩子,则该饼干也一定不能满足需求因子更大的孩子;
- 某个孩子可以用更小的饼干满足,则没必要用更大的饼干满足,因为可以保留更大的饼干满足需求因子更大的孩子;
- 孩子的需求因子更小则其更容易被满足,故优先从需求因子小的孩子尝试,可以得到正确的结果。
贪心策略:对需求因子数组 g 和饼干大小数组 s 进行从小到大的排序,按照从小到大的顺序使用个饼干尝试是否可以满足某个孩子,每个饼干只尝试一次,若尝试成功,则换下一个孩子尝试,直到发现没更多孩子或者没更多的饼干,循环结束。
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
g.sort()
s.sort()
gi,si=0,0
while gi<len(g) and si<len(s):
if g[gi]<=s[si]:
gi+=1
si+=1
return gi
376.摆动序列【中等】
LeetCode传送门
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
思路: 贪心+有限状态机
当序列有一段连续的递增(或递减)时,为形成摇摆子序列,只需保留这段连续的递增(或递减)的首尾元素,这样更可能使得尾部的后一个元素成为摇摆子序列的下一个元素。
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
if len(nums)<2:
return len(nums)
BEGIN,UP,DOWN=0,1,2
STATE=BEGIN
max_length=1
for i in range(1,len(nums)):
if STATE==BEGIN:
if nums[i]>nums[i-1]:
STATE=UP
max_length+=1
elif nums[i]<nums[i-1]:
STATE=DOWN
max_length+=1
elif STATE==UP:
if nums[i]<nums[i-1]:
STATE=DOWN
max_length+=1
elif STATE==DOWN:
if nums[i]>nums[i-1]:
STATE=UP
max_length+=1
return max_length
402.移除 k 个数字【中等】
LeetCode传送门
给定一个以字符串表示的非负整数 num,移除这个数中的 k 位数字,使得剩下的数字最小。
注意:
- num 的长度小于 10002 且 ≥ k。
- num 不会包含任何前导零。
思路: 贪心+栈
若去掉某一位数字,为了使得到的新数字最小,需要尽可能让得到的新数字优先最高位最小,其次次高位最小,……
- 暴力:从高位向低位遍历,如果对应的数字大于下一位数字,则把该位数字去掉,得到的数字最小。去掉 k 个数字,则从最高位遍历 k 次。
- 使用栈存储最终结果或删除工作:从高位向低位遍历 num,
- 如果栈为空或遍历的数字大于栈顶元素,则将该数字push入栈,需要注意,如果栈为空且数字为0(如100200),则不入栈;
- 如果栈不空,且当前遍历元素小于栈顶元素则进行pop弹栈,直到栈为空或不能再删除数字(已删除 k 位)或栈顶元素小于当前元素为止。
- 如果所有数字都扫描完成后,k 仍然大于0,则进行pop弹栈,直到删除 k 个元素;
- 最后结果即从栈底遍历至栈顶。
class Solution:
def removeKdigits(self, num: str, k: int) -> str:
stack=[]
for i in range(len(num)):
number=int(num[i])
while stack and number<stack[-1] and k>0:
stack.pop()
k-=1
if stack or number!=0:[添加链接描述](https://leetcode-cn.com/problems/jump-game/)
stack.append(number)
while stack and k>0:
stack.pop()
k-=1
result=''.join([str(c) for c in stack])
if not result:
result="0"
return result
55.跳跃游戏【中等】
LeetCode传送门
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
思路:
贪心思想:若从位置0最远可以跳至第 i 个位置,则从位置0也可以跳至位置1、位置2、……、位置i-1,假设从这些位置又可以向前跳至的位置分别为 index[1]、index[2]、……、index[i-1],则从位置0,应该跳至index[1]、index[2]、……、index[i-1]中最大的那个。
- 计算从位置 i 最远可跳至的位置: i n d e x [ i ] = n u m s [ i ] + i index[i]=nums[i]+i index[i]=nums[i]+i;
- 设置变量 jump 代表当前所处的位置,初始化为0;
- 设置变量 max_index 代表从位置0至位置jump这个过程中,最远可到达的位置,初始化为index[0];
- 利用jump扫描index数组,直到jump达到index数组尾部或jump超过max_index,扫描过程中,更新max_index;
- 若最终jump为数组长度,则返回true,否则返回false。
class Solution:
def canJump(self, nums: List[int]) -> bool:
index=[nums[i]+i for i in range(len(nums))]
jump=0
max_index=nums[0]
while jump<len(index) and jump<=max_index:
if max_index<index[jump]:
max_index=index[jump]
jump+=1
if jump==len(index):
return True
return False
45.跳跃游戏2【困难】
LeetCode传送门
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
思路:
在到达某点前若一直不跳跃,发现从该点不能跳跃到更远的地方,在这之前肯定有次必要的跳跃。也就是说,在无法到达更远的地方时,在这之前应该跳到一个可以到达更远位置的位置。算法:
- 设置 c u r r e n t _ m a x _ i n d e x current\_max\_index current_max_index 为当前可达到的最远位置(最小的跳跃次数下);
- 设置 p r e _ m a x _ i n d e x pre\_max\_index pre_max_index 为在遍历各个位置的过程中,各个位置可达到的最远位置;
- 设置 j u m p _ m i n jump\_min jump_min 为最少跳跃次数;
- 利用 i i i 遍历数组 n u m s nums nums,若 i i i 超过 c u r r e n t _ m a x _ i n d e x current\_max\_index current_max_index,也就是说,此时不能再向前走,必须在当前位置及之前的某个位置跳了,这个位置是当前遍历的位置中能达到的最远位置中最大的,执行跳跃次数加1 j u m p _ m i n + = 1 jump\_min+=1 jump_min+=1,更新当前能调到的最远位置 c u r r e n t _ m a x _ i n d e x = p r e _ m a x _ i n d e x current\_max\_index=pre\_max\_index current_max_index=pre_max_index;
- 遍历过程中,若 n u m s [ i ] + i nums[i]+i nums[i]+i更大,更新当前能到达的最远位置 p r e _ m a x _ i n d e x = n u m s [ i ] + i pre\_max\_index=nums[i]+i pre_max_index=nums[i]+i。
class Solution:
def jump(self, nums: List[int]) -> int:
if len(nums)<2:
return 0
jump_min=1
cur_max_index=nums[0]
pre_max_index=nums[0]
for i in range(len(nums)):
if i>cur_max_index:
jump_min+=1
cur_max_index=pre_max_index
pre_max_index=max(pre_max_index,nums[i]+i)
return jump_min
452.用最少数量的箭引爆气球【中等】
LeetCode传送门
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在104个气球。
一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
思路:
- 对各个气球进行排序,按照气球的左端点从小到大排序;
- 遍历气球数组,同时维护一个射击区间 [ s h o o t _ b e g i n , s h o o t _ e n d ] [shoot\_begin,shoot\_end] [shoot_begin,shoot_end],在满足可以将当前气球射穿的情况下,尽可能击穿更多的气球,每击穿一个新的气球,更新一次射击区间(保证射击区间可以将新气球也击穿);
- 如果新的气球没办法被击穿了,则需要增加一名弓箭手,即维护一个新的射击区间将该气球击穿,随后继续遍历气球数组。
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
if len(points)==0:
return 0
points.sort()
shoot_num=1
shoot_begin=points[0][0]
shoot_end=points[0][1]
for i in range(1,len(points)):
if shoot_end>=points[i][0]:
shoot_begin=points[i][0]
if shoot_end>points[i][1]:
shoot_end=points[i][1]
else:
shoot_num+=1
shoot_begin=points[i][0]
shoot_end=points[i][1]
return shoot_num
871.最低加油次数【困难】
汽车从起点出发驶向目的地,该目的地位于出发位置东面 target 英里处。
沿途有加油站,每个 station[i] 代表一个加油站,它位于出发位置东面 station[i][0] 英里处,并且有 station[i][1] 升汽油。
假设汽车油箱的容量是无限的,其中最初有 startFuel 升燃料。它每行驶 1 英里就会用掉 1 升汽油。
当汽车到达加油站时,它可能停下来加油,将所有汽油从加油站转移到汽车中。
为了到达目的地,汽车所必要的最低加油次数是多少?如果无法到达目的地,则返回 -1 。
注意:如果汽车到达加油站时剩余燃料为 0,它仍然可以在那里加油。如果汽车到达目的地时剩余燃料为 0,仍然认为它已经到达目的地。
思路:
- 设置一个最大堆,用来存储经过的加油站的汽油量;
- 按照从起点至终点的方向,遍历各个加油站之间的距离;
- 每次需要走两个加油站之间的距离 dist,如果发现汽油不够走距离 dist 时,从最大堆中取出一个油量添加,直到可以足够走距离 dist;
- 如果把最大堆的汽油都添加仍然不够行进距离 dist,则无法达到终点。
- 每次遍历时,当前油量startFuel减dist,将当前加油站油量添加至最大堆。
class Solution:
def minRefuelStops(self, target: int, startFuel: int, stations: List[List[int]]) -> int:
from heapq import heappush
from heapq import heappop
pqueue,ans=[],0
stations.append([target,0])
# stations.sort()
for i in range(len(stations)):
dist=stations[i][0]-stations[i-1][0] if i!=0 else stations[i][0]
while pqueue and startFuel<dist:
startFuel-=heappop(pqueue)
ans+=1
if not pqueue and startFuel<dist:
return -1
heappush(pqueue,-stations[i][1])
startFuel-=dist
return ans