提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一、贪心算法
贪心算法是对完成一件事情的方法的描述,贪心算法每一次都做出当前看起来最好的选择,而不用考虑其它可能的选择。
- 贪心算法与回溯算法、动态规划的区别
- 「回溯算法」需要记录每一个步骤、每一个选择,用于回答所有具体解的问题;
- 「动态规划」需要记录的是每一个步骤、所有选择的汇总值(最大、最小或者计数);
- 「贪心算法」由于适用的问题,每一个步骤只有一种选择,一般而言只需要记录与当前步骤相关的变量的值。
- 可以使用「贪心算法」的问题需要满足的条件
- 最优子结构:规模较大的问题的解由规模较小的子问题的解组成,区别于「动态规划」,可以使用「贪心算法」的问题「规模较大的问题的解」只由其中一个「规模较小的子问题的解」决定;
- 无后效性:后面阶段的求解不会修改前面阶段已经计算好的结果;
- 贪心选择性质:从局部最优解可以得到全局最优解。(如从10张钞票中选5次,使得到总额最大,那么我每次选取最大(局部最优),最后总额也是最大的),贪心其实和常识没啥区别(模拟)
对「最优子结构」和「无后效性」的理解同「动态规划」,「贪心选择性质」是「贪心算法」最需要关注的内容。
总结:
- 「贪心算法」总是做出在当前看来最好的选择就可以完成任务;
- 解决「贪心算法」几乎没有套路,到底如何贪心,贪什么与我们要解决的问题密切相关。因此刚开始学习「贪心算法」的时候需要学习和模仿,然后才有直觉,猜测一个问题可能需要使用「贪心算法」,进而尝试证明,学会证明。
二、题库
1.简
455. 分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
# 思路:排序,先从胃口最大的开始给,如果没有满足的饼干,则跳入下一个小孩子
i = len(g) - 1
j = len(s) - 1
cnt = 0
g.sort()
s.sort()
while min(i,j) >= 0:
if s[j] >= g[i]:
cnt += 1
j -= 1
i -= 1
return cnt
复杂度分析
-
时间复杂度:O(mlogm+nlogn),其中 m 和 n 分别是数组 g 和 s 的长度。对两个数组排序的时间复杂度是 O(mlogm+nlogn),遍历数组的时间复杂度是 O(m+n),因此总时间复杂度是 O(mlogm+nlogn)。
-
空间复杂度:O(logm+logn),其中 m 和 n 分别是数组 g 和 s 的长度。空间复杂度主要是排序的额外空间开销。
860. 柠檬水找零
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
示例 1:
输入:bills = [5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。
class Solution:
def lemonadeChange(self, bills: List[int]) -> bool:
# 贪心,每一次选择一次,就和模拟相似
five=0
ten=0
for i in bills:
if i==5:
five+=1
elif i==10:
five-=1
ten+=1
else:
if ten>=1:
ten-=1
five-=1
else:
five-=3
if five<0 or ten<0:
return False
return True
复杂度分析
-
时间复杂度:O(n),
-
空间复杂度:O(1)
2.中
435. 无重叠区间
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例 1:
输入: [ [1,2], [2,3], [3,4], [1,3] ]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
思路:
先原区间(右节点)进行排序,我们可以不断地寻找右端点在首个区间右端点左侧的新区间,将首个区间替换成该区间。那么当我们无法替换时,首个区间就是所有可以选择的区间中右端点最小的那个区间。因此我们将所有区间按照右端点从小到大进行排序,那么排完序之后的首个区间,就是我们选择的首个区间。就是每一次找到符合的,最后进行总数相减。
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
# 贪心,每一次选择最优
intervals.sort(key=lambda x: x[1])
right=intervals[0][1]
ans=1
for i in range(1,len(intervals)):
if intervals[i][0]>=right:
ans+=1
right=intervals[i][1]
return len(intervals)-ans
复杂度分析
- 时间复杂度:O(nlogn),其中 n 是区间的数量。我们需要 O(nlogn) 的时间对所有的区间按照右端点进行升序排序,并且需要 O(n) 的时间进行遍历。由于前者在渐进意义下大于后者,因此总时间复杂度为 O(nlogn)。
- 空间复杂度:O(logn),即为排序需要使用的栈空间。
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
# 动态规划:对itervals进行排序,对每一个区间进行遍历,进行统计其当前区间作为最后区间满足的区间个数:
# 最优子结构: dp[i]=max(dp[j] if itervals[i][0]>=itervals[j][1])+1
# 初始条件:dp[0]=1
if not intervals:
return 0
intervals.sort()
n = len(intervals)
f = [1]
for i in range(1, n):
f.append(max((f[j] for j in range(i) if intervals[j][1] <= intervals[i][0]), default=0) + 1)
return n - max(f)
复杂度分析
-
时间复杂度:O(n^2),
-
空间复杂度:O(logn),即为排序需要使用的栈空间。
452. 用最少数量的箭引爆气球
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。
一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。
示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
# 思路:贪心,就是寻找无重叠区间的个数,每个每个最优解进行求解,
points.sort(key=lambda x :x[1])
right=points[0][1]
ans=1
for i in range(1,len(points)):
if points[i][0]>right:
ans+=1
right=points[i][1]
return ans
复杂度分析
- 时间复杂度:O(nlogn),其中 n 是区间的数量。我们需要 O(nlogn) 的时间对所有的区间按照右端点进行升序排序,并且需要 O(n) 的时间进行遍历。由于前者在渐进意义下大于后者,因此总时间复杂度为 O(nlogn)。
- 空间复杂度:O(logn),即为排序需要使用的栈空间。
1536. 排布二进制网格的最少交换次数
给你一个 n x n 的二进制网格 grid,每一次操作中,你可以选择网格的 相邻两行 进行交换。
一个符合要求的网格需要满足主对角线以上的格子全部都是 0 。
请你返回使网格满足要求的最少操作次数,如果无法使网格符合要求,请你返回 -1 。
主对角线指的是从 (1, 1) 到 (n, n) 的这些格子。
示例 1:
输入:grid = [[0,0,1],[1,1,0],[1,0,0]]
输出:3
class Solution:
def minSwaps(self, grid: List[List[int]]) -> int:
# 1. 思路:贪心 时间复杂度O(n^2) 空间 o(n)
pos=[0]*len(grid)
n=len(grid)
for i in range(n):
for j in range(n-1,-1,-1):
if grid[i][j]==1:
pos[i]=j
break
cnt=0
for i in range(n):# 贪心 对每行都保证,正确
exchange=-1
if pos[i]>i:
for j in range(i,len(pos)):
if pos[j]<=i:# 则找到应该交换的位置了
exchange=j
cnt+=exchange-i
break
if exchange==-1:
return -1
for j in range(exchange,i,-1): # 开始进行两两交换
pos[j],pos[j-1]=pos[j-1],pos[j]
else:
continue
return cnt
56. 合并区间
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
# 和前面两个不同,前两个无论是对哪一个位置排序都行,但是合并区间,对第二个位置排序,解决不了,
intervals.sort(key=lambda x :x[0])
ans=[intervals[0]]
for i in range(1,len(intervals)):
if intervals[i][0]<=ans[-1][1]:
ans[-1][0]=min(ans[-1][0],intervals[i][0])
ans[-1][1]=max(ans[-1][1],intervals[i][1])
else:
ans.append(intervals[i])
return ans
复杂度分析
- 时间复杂度:O(nlogn),其中 n 是区间的数量。我们需要 O(nlogn) 的时间对所有的区间按照右端点进行升序排序,并且需要 O(n) 的时间进行遍历。由于前者在渐进意义下大于后者,因此总时间复杂度为 O(nlogn)。
- 空间复杂度:O(logn),即为排序需要使用的栈空间。
55. 跳跃游戏
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
class Solution:
def canJump(self, nums: List[int]) -> bool:
# 思路1:贪心,每一步,都是当前走过的最优步数, i+nums[i] i表示我已走的步数,nums[i]表示当前第i步我可以跳跃的的最大长度,因而相加就等价于,整体跳跃的最大长度
n, rightmost = len(nums), 0
len_nums=len(nums)
for i in range(len_nums):
if i<=rightmost:# 只有当,rightmost>=i 则表明能走到第i步,才可以计算。
rightmost=max(rightmost,i+nums[i])
print(i,rightmost)
if rightmost>=len_nums-1:
return True
return False
复杂度分析
- 时间复杂度:O(n),
- 空间复杂度:O(1),
45. 跳跃游戏 II
给你一个非负整数数组 nums ,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
假设你总是可以到达数组的最后一个位置。
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
贪心的思路其实就是,如我在索引为0这个点,开始走,其走的步数是二,那么有一个特点,他的下一步是有两个值为3,1,则进行计算3和1这两个点,能到达最远的距离,即3+1,和2+1 进行比较,
class Solution:
def jump(self, nums: List[int]) -> int:
# 思路:贪心 i+nums[i]指的是当前能走的最远距离
nums_len=len(nums)
positon=0
step=0
end=0
for i in range(nums_len-1):
if positon>=i:
positon=max(positon,i+nums[i]) #
if i==end:
end=positon
step+=1
return step
复杂度分析
-
时间复杂度:O(n),其中 n 是数组长度。
-
空间复杂度:O(1)。
763. 划分字母区间
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
示例:
输入:S = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释:
划分结果为 “ababcbaca”, “defegde”, “hijhklij”。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 的划分是错误的,因为划分的片段数较少。
class Solution:
def partitionLabels(self, s: str) -> List[int]:
# 贪心加双指针 将每一个字母最后一次出现的索引记性存储,其次遍历整个数组,end表示走到的最长路径,当遍历的路径和走路的路径最长相等时,表明当前已经走到可以分块了。 时间复杂度为o(n) 空间复杂度为 o(26)
last=[0]*26
for index in range(len(s)):
last[ord(s[index])-ord('a')]=index
partition=[]
start=end=0
for index,val in enumerate(s):
end=max(end,last[ord(val)-ord('a')])
if index==end:
partition.append(end-start+1)
start=end+1
return partition
三、基础与练习
392. 判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
进阶:
如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
示例 1:
输入:s = “abc”, t = “ahbgdc”
输出:true
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
# 思路:贪心,每次匹配到s,则s索引往下移
if len(s)==0:
return True
s_index=0
flag=False
for i in t:
if i==s[s_index]:
s_index+=1
if s_index==len(s):
flag=True
break
return flag
复杂度分析
-
时间复杂度:O(n),其中 n 是数组长度。
-
空间复杂度:O(1)。
561. 数组拆分 I
给定长度为 2n 的整数数组 nums ,你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), …, (an, bn) ,使得从 1 到 n 的 min(ai, bi) 总和最大。
返回该 最大总和 。
示例 1:
输入:nums = [1,4,3,2]
输出:4
解释:所有可能的分法(忽略元素顺序)为:
- (1, 4), (2, 3) -> min(1, 4) + min(2, 3) = 1 + 2 = 3
- (1, 3), (2, 4) -> min(1, 3) + min(2, 4) = 1 + 2 = 3
- (1, 2), (3, 4) -> min(1, 2) + min(3, 4) = 1 + 3 = 4
所以最大总和为 4
class Solution:
def arrayPairSum(self, nums: List[int]) -> int:
# 排序,直接取奇数位的值
nums.sort()
ans=0
for i in range(len(nums)):
if i%2==0:
ans+=nums[i]
return ans
复杂度分析
-
时间复杂度:O(nlogn),其中 n 是数组长度。
-
空间复杂度:O(logn)。排序需要栈的空间
1710. 卡车上的最大单元数
请你将一些箱子装在 一辆卡车 上。给你一个二维数组 boxTypes ,其中 boxTypes[i] = [numberOfBoxesi, numberOfUnitsPerBoxi] :
numberOfBoxesi 是类型 i 的箱子的数量。
numberOfUnitsPerBoxi 是类型 i 每个箱子可以装载的单元数量。
整数 truckSize 表示卡车上可以装载 箱子 的 最大数量 。只要箱子数量不超过 truckSize ,你就可以选择任意箱子装到卡车上。
返回卡车可以装载 单元 的 最大 总数。
示例 1:
输入:boxTypes = [[1,3],[2,2],[3,1]], truckSize = 4
输出:8
解释:箱子的情况如下:
- 1 个第一类的箱子,里面含 3 个单元。
- 2 个第二类的箱子,每个里面含 2 个单元。
- 3 个第三类的箱子,每个里面含 1 个单元。
可以选择第一类和第二类的所有箱子,以及第三类的一个箱子。
单元总数 = (1 * 3) + (2 * 2) + (1 * 1) =
class Solution:
def maximumUnits(self, boxTypes: List[List[int]], truckSize: int) -> int:
# 贪心:排序,每一次获取单元数量最多的箱子尽量装满
boxTypes.sort(key=lambda x:x[1],reverse=True)
ans=0
for i in boxTypes:
if truckSize-i[0]>=0:
ans+=i[0]*i[1]
truckSize-=i[0]
else:
ans+=truckSize*i[1]
return ans
return ans
复杂度分析
-
时间复杂度:O(nlogn),其中 n 是数组长度。
-
空间复杂度:O(logn)。排序需要栈的空间
1217. 玩筹码
数轴上放置了一些筹码,每个筹码的位置存在数组 chips 当中。
你可以对 任何筹码 执行下面两种操作之一(不限操作次数,0 次也可以):
将第 i 个筹码向左或者右移动 2 个单位,代价为 0。
将第 i 个筹码向左或者右移动 1 个单位,代价为 1。
最开始的时候,同一位置上也可能放着两个或者更多的筹码。
返回将所有筹码移动到同一位置(任意位置)上所需要的最小代价。
示例 1:
输入:chips = [1,2,3]
输出:1
解释:第二个筹码移动到位置三的代价是 1,第一个筹码移动到位置三的代价是 0,总代价为 1。
class Solution:
def minCostToMoveChips(self, position: List[int]) -> int:
# 统计奇偶个数,返回数量最小的
even=0
odd=0
for i in position:
if i%2:
odd+=1
else:
even+=1
return min(odd,even)
复杂度分析
-
时间复杂度:O(n),其中 n 是数组长度。
-
空间复杂度:O(1)。
中
122. 买卖股票的最佳时机 II
给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
# 思路:贪心,由题目可以很容易得出只要当前价格比上一次价格有提升,则购买和卖出
price=0
for i in range(1,len(prices)):
temp=prices[i]-prices[i-1]
if temp>0:
price+=temp
return price
复杂度分析
-
时间复杂度:O(n),其中 n 是数组长度。
-
空间复杂度:O(1)。
2038. 如果相邻两个颜色均相同则删除当前颜色
总共有 n 个颜色片段排成一列,每个颜色片段要么是 ‘A’ 要么是 ‘B’ 。给你一个长度为 n 的字符串 colors ,其中 colors[i] 表示第 i 个颜色片段的颜色。
Alice 和 Bob 在玩一个游戏,他们 轮流 从这个字符串中删除颜色。Alice 先手 。
如果一个颜色片段为 ‘A’ 且 相邻两个颜色 都是颜色 ‘A’ ,那么 Alice 可以删除该颜色片段。Alice 不可以 删除任何颜色 ‘B’ 片段。
如果一个颜色片段为 ‘B’ 且 相邻两个颜色 都是颜色 ‘B’ ,那么 Bob 可以删除该颜色片段。Bob 不可以 删除任何颜色 ‘A’ 片段。
Alice 和 Bob 不能 从字符串两端删除颜色片段。
如果其中一人无法继续操作,则该玩家 输 掉游戏且另一玩家 获胜 。
假设 Alice 和 Bob 都采用最优策略,如果 Alice 获胜,请返回 true,否则 Bob 获胜,返回 false。
示例 1:
输入:colors = “AAABABB”
输出:true
解释:
AAABABB -> AABABB
Alice 先操作。
她删除从左数第二个 ‘A’ ,这也是唯一一个相邻颜色片段都是 ‘A’ 的 ‘A’ 。
现在轮到 Bob 操作。
Bob 无法执行任何操作,因为没有相邻位置都是 ‘B’ 的颜色片段 ‘B’ 。
因此,Alice 获胜,返回 true 。
class Solution:
def winnerOfGame(self, colors: str) -> bool:
# 贪心,时间o(n) 空间o(1)
dict_colors=[0,0]
curr,cnt='C',0
for c in colors:
if c!=curr:
cnt=1
curr=c
else:
cnt+=1
if cnt>=3:
dict_colors[ord(curr)-ord('A')]+=1
if dict_colors[0]>dict_colors[1]:
return True
return False