Datawhale Leecode基础算法篇 task04:贪心算法
官方学习文档:datawhalechina
往期task01:枚举算法链接:Datawhale Leecode基础算法篇 task01:枚举算法
往期task02:递归算法and分治算法:Datawhale Leecode基础算法篇 task02:递归算法and分治算法
往期task03:回溯算法:Datawhale Leecode基础算法篇 task03:回溯算法
贪心算法
贪心算法简介
贪心算法(Greedy Algorithm):一种在每次决策时,总是采取在当前状态下的最好选择,从而希望导致结果是最好或最优的算法。
贪心算法是一种改进的「分步解决算法」,其核心思想是:将求解过程分成「若干个步骤」,然后根据题意选择一种「度量标准」,每个步骤都应用「贪心原则」,选取当前状态下「最好 / 最优选择(局部最优解)」,并以此希望最后得出的结果也是「最好 / 最优结果(全局最优解)」。
换句话说,贪心算法不从整体最优上加以考虑,而是一步一步进行,每一步只以当前情况为基础,根据某个优化测度做出局部最优选择,从而省去了为找到最优解要穷举所有可能所必须耗费的大量时间。
不是所有问题,都可以使用贪心算法通过局部最优解而得到整体最优解或者是整体最优解的近似解。使用贪心算法解决的问题必须满足下面的两个特征:
- 贪心选择性
- 最优子结构
贪心选择性:指的是一个问题的全局最优解可以通过一系列局部最优解(贪心选择)来得到。
换句话说,当进行选择时,我们直接做出在当前问题中看来最优的选择,而不用去考虑子问题的解。在做出选择之后,才会去求解剩下的子问题,如下图所示。
贪心算法在进行选择时,可能会依赖之前做出的选择,但不会依赖任何将来的选择或是子问题的解。运用贪心算法解决的问题在程序的运行过程中无回溯过程。
最优子结构性质:指的是一个问题的最优解包含其子问题的最优解。
问题的最优子结构性质是该问题能否用贪心算法求解的关键。
如果原问题的最优解包含子问题的最优解,则说明该问题满足最优子结构性质。
反之,如果不能利用子问题的最优解推导出整个问题的最优解,那么这种问题就不具有最优子结构。
贪心算法最难的部分不在于问题的求解,而在于正确性的证明。我们常用的证明方法有「数学归纳法」和「交换论证法」。
数学归纳法:先计算出边界情况(例如 n=1)的最优解,然后再证明对于每个 n,都可以由 Fn 推导出。
交换论证法:从最优解出发,在保证全局最优不变的前提下,如果交换方案中任意两个元素 / 相邻的两个元素后,答案不会变得更好,则可以推定目前的解是最优解。
判断一个问题是否通过贪心算法求解,是需要进行严格的数学证明的。但是在日常写题或者算法面试中,不太会要求大家去证明贪心算法的正确性。
所以,当我们想要判断一个问题是否通过贪心算法求解时,我们可以:
- 凭直觉:如果感觉这道题可以通过「贪心算法」去做,就尝试找到局部最优解,再推导出全局最优解。
- 举反例:尝试一下,举出反例。也就是说找出一个局部最优解推不出全局最优解的例子,或者找出一个替换当前子问题的最优解,可以得到更优解的例子。如果举不出反例,大概率这道题是可以通过贪心算法求解的。
贪心算法三步走
- 转换问题:将优化问题转换为具有贪心选择性质的问题,即先做出选择,再解决剩下的一个子问题。
- 贪心选择性质:根据题意选择一种度量标准,制定贪心策略,选取当前状态下「最好 / 最优选择」,从而得到局部最优解。
- 最优子结构性质:根据上一步制定的贪心策略,将贪心选择的局部最优解和子问题的最优解合并起来,得到原问题的最优解。
贪心算法的应用
分发饼干
题目链接:
- 455. 分发饼干 - 力扣
描述:一位很棒的家长为孩子们分发饼干。对于每个孩子 i,都有一个胃口值 g[i],即每个小孩希望得到饼干的最小尺寸值。对于每块饼干 j,都有一个尺寸值 s[j]。只有当 s[j]>=g[i] 时,我们才能将饼干 j 分配给孩子 i。每个孩子最多只能给一块饼干。
现在给定代表所有孩子胃口值的数组 g 和代表所有饼干尺寸的数组 j。
要求:尽可能满足越多数量的孩子,并求出这个最大数值。
说明:
- 1≤g.length≤3∗104。
- 0≤s.length≤3∗104。
- 1≤g[i],s[j]≤231−1。
示例:
- 示例 1:
输入:g = [1,2,3], s = [1,1] 输出:1 解释:你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1, 2, 3。虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。所以应该输出 1。
- 示例 2:
输入: g = [1,2], s = [1,2,3] 输出: 2 解释: 你有两个孩子和三块小饼干,2个孩子的胃口值分别是1, 2。你拥有的饼干数量和尺寸都足以让所有孩子满足。所以你应该输出 2。
解题思路:
为了尽可能的满⾜更多的⼩孩,而且一块饼干不能掰成两半,所以我们应该尽量让胃口小的孩子吃小块饼干,这样胃口大的孩子才有大块饼干吃。
所以,从贪心算法的角度来考虑,我们首先应该对数组 g 和s进行排序,并且对于每个孩子,应该选择满足这个孩子的胃口且尺寸最小的饼干。
下面我们使用贪心算法三步走的方法解决这道题。
- 转换问题:将原问题转变为,当胃口最小的孩子选择完满足这个孩子的胃口且尺寸最小的饼干之后,再解决剩下孩子的选择问题(子问题)。
- 贪心选择性质:对于当前孩子,用尺寸尽可能小的饼干满足这个孩子的胃口。
- 最优子结构性质:在上面的贪心策略下,当前孩子的贪心选择 + 剩下孩子的子问题最优解,就是全局最优解。也就是说在贪心选择的方案下,能够使得满足胃口的孩子数量达到最大。
使用贪心算法的代码解决步骤描述如下:
- 对数组 g、s 进行从小到大排序,使用变量 index_g 和 index_s 分别指向 g、s 初始位置,使用变量 res 保存结果,初始化为 0。
- 对比每个元素 g[index_g] 和 s[index_s]:
- 如果 g[index_g]≤s[index_s] ,说明当前饼干满足当前孩子胃口,则答案数量加 1,并且向右移动 index_g 和 index_s。
- 如果 g[index_g]>s[index_s],说明当前饼干无法满足当前孩子胃口,则向右移动 indexs,判断下一块饼干是否可以满足当前孩子胃口。
- 遍历完输出答案 res。
代码:
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
g.sort()
s.sort()
index_g, index_s = 0, 0
res = 0
while index_g < len(g) and index_s < len(s):
if g[index_g] <= s[index_s]:
res += 1
index_g += 1
index_s += 1
else:
index_s += 1
return res
无重叠区间
题目链接:
- 435. 无重叠区间 - 力扣
描述:给定一个区间的集合 intervals,其中 intervals[i]=[starti,endi]。从集合中移除部分区间,使得剩下的区间互不重叠(指在数轴上的区间没有重叠区域)。
要求:返回需要移除区间的最小数量。
说明:
- 1≤intervals.length≤105。
- intervals[i].length==2。
- −5∗104≤starti<endi≤5∗104。
示例:
- 示例 1:
输入:intervals = [[1,2],[2,3],[3,4],[1,3]] 输出:1 解释:移除 [1,3] 后,剩下的区间没有重叠。
- 示例 2:
输入: intervals = [ [1,2], [1,2], [1,2] ] 输出: 2 解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
解题思路:
那我们思考怎么才能让区间不重叠呢?下一个区间的开头大于等于上个区间的结尾吗?那怎么使用贪心算法来解决这个问题呢?
从正着的角度,移除最少几个区间来使剩下的区间没有重合,看起来很难实现,那我们能不能倒过来想呢,让没有重合的区间数最多,总数与之相减就是所求,会不会简单一点?
从贪心算法的角度来考虑,我们首先应该将区间按照结束时间排序。每次选择结束时间最早的区间,然后再在剩下的时间内选出最多的不重叠区间。
我们用贪心三部曲来解决这道题。
- 转换问题:将原问题转变为,当选择结束时间最早的区间之后,再在剩下的时间内选出最多的区间(子问题)。
- 贪心选择性质:每次选择时,选择结束时间最早的区间。这样选出来的区间一定是原问题最优解的区间之一。
- 最优子结构性质:在上面的贪心策略下,贪心选择当前时间最早的区间 + 剩下的时间内选出最多不重叠区间的子问题最优解,就是全局最优解。
使用贪心算法的代码解决步骤描述如下:
- 将区间集合按照结束坐标升序排列,然后定义两个变量,一个是当前不重叠区间的结束时间 end_pos,另一个是不重叠区间的个数 count。初始情况下,结束坐标 end_pos 为第一个区间的结束坐标,count 为 1。
- 依次遍历每段区间。对于每段区间:intervals[i]:
- 如果 end_pos≤intervals[i][0],即 end_pos 小于等于区间起始位置,则说明出现了不重叠区间,令不重叠区间数 count 加 1,end_pos 更新为新区间的结束位置。
- 最终返回「总区间个数 - 不重叠区间的最多个数」即 len(intervals)−count 作为答案。
代码:
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
if not intervals:
return 0
intervals.sort(key=lambda x: x[1])
end_pos = intervals[0][1]
count = 1
for i in range(1, len(intervals)):
if end_pos <= intervals[i][0]:
count += 1
end_pos = intervals[i][1]
return len(intervals) - count
首先判断数组是否为空,为空则返回0,然后根据每个区间的结束时间对区间进行排序,end_pos为排序后第一个区间的结束时间,然后进行遍历即可。
练习
柠檬水找零
题目链接:柠檬水找零
描述:一杯柠檬水的售价是 5 美元。现在有 n 个顾客排队购买柠檬水,每人只能购买一杯。顾客支付的钱面额有 5 美元、10 美元、20 美元。必须给每个顾客正确找零(就是每位顾客需要向你支付 5 美元,多出的钱要找还回顾客)。
现在给定 n 个顾客支付的钱币面额数组 bills
。
要求:如果能给每位顾客正确找零,则返回 True
,否则返回 False
。
说明:
- 一开始的时候手头没有任何零钱。
- 1≤bills.length≤。
bills[i]
不是 5 就是 10 或是 20。
示例:
- 示例 1:
输入:bills = [5,5,5,10,20]
输出:True
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 True。
- 示例 2:
输入:bills = [5,5,10,10,20]
输出:False
解释:
前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
由于不是每位顾客都得到了正确的找零,所以答案是 False。
解题思路:
首先,我们看这道题可能会陷入一个思维误区,既然5美元最重要(因其可以用于10和20美元的找零),10美元其次,20美元最后,那按照贪心算法是不是开始5美元越多越好,先要把bills进行一个排序呢?
错,因为这个问题里顾客可能不是同时来的,先来的就必须先做。
那么我们就可以决定对于不同数额分别采取不同策略:
- 如果顾客支付 5 美元,直接收下。
- 如果顾客支付 10 美元,如果我们手头有 5 美元面额的钞票,则找给顾客,否则无法正确找零,返回
False
。 - 如果顾客支付 20 美元,如果我们手头有 1 张 10 美元和 1 张 5 美元的钞票,或者有 3 张 5 美元的钞票,则可以找给顾客。如果两种组合方式同时存在,倾向于第 1 种方式找零,因为使用 5 美元的场景比使用 10 美元的场景多,要尽可能的保留 5 美元的钞票。如果这两种组合方式都不通知,则无法正确找零,返回
False
。
代码:
class Solution:
def lemonadeChange(self, bills: List[int]) -> bool:
five, ten, twenty = 0, 0, 0
for bill in bills:
if bill == 5:
five += 1
if bill == 10:
if five <= 0:
return False
ten += 1
five -= 1
if bill == 20:
if five > 0 and ten > 0:
five -= 1
ten -= 1
twenty += 1
elif five >= 3:
five -= 3
twenty += 1
else:
return False
return True
分发糖果
题目链接:分发糖果
描述:n 个孩子站成一排。老师会根据每个孩子的表现,给每个孩子进行评分。然后根据下面的规则给孩子们分发糖果:
- 每个孩子至少得 1 个糖果。
- 评分更高的孩子必须比他两侧相邻位置上的孩子分得更多的糖果。
现在给定 n 个孩子的表现分数数组 ratings
,其中 ratings[i]
表示第 i 个孩子的评分。
要求:返回最少需要准备的糖果数目。
说明:
- n==ratings.length。
- 1≤n≤2×。
- 0≤ratings[i]≤2∗。
示例:
- 示例 1:
输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
- 示例 2:
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
解题思路:
首先,我们观察示例1发现,孩子的顺序是不能变动的,否则可以将ratings变为[1,2,0],分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果,这样总数就是4了,小于输出的5,不符合题目要求。
那我们怎么满足评分更高的孩子必须比他两侧相邻位置上的孩子分得更多的糖果这个条件呢?分别从前往后和从后往前遍历处理一遍可以吗?从后往前遍历处理会影响之前的步骤吗?
思考后发现,其实是不会的,因为从后往前遍历处理只可能让评分更高的孩子的糖果数更高,而不会影响评分更低的孩子。
这样我们就可以制定规则:
「每个孩子至少得 1 个糖果」:说明糖果数目至少为 N 个。
「评分更高的孩子必须比他两侧相邻位置上的孩子分得更多的糖果」:可以看做为以下两种条件:
- 当 ratings[i−1]<ratings[i] 时,第 i 个孩子的糖果数量要比第 i−1 个孩子的糖果数量多;
- 当 ratings[i]>ratings[i+1] 时,第 i 个孩子的糖果数量要比第i+1 个孩子的糖果数量多。
根据以上信息,我们可以设定一个长度为 N 的数组 sweets 来表示每个孩子分得的最少糖果数,初始每个孩子分得糖果数都为 1。
然后遍历两遍数组,第一遍遍历满足当 ratings[i−1]<ratings[i] 时,第 i 个孩子的糖果数量比第 i−1 个孩子的糖果数量多 1 个。第二遍遍历满足当 ratings[i]>ratings[i+1] 时,第 i 个孩子的糖果数量取「第 i+1 个孩子的糖果数量多 1 个」和「第 i 个孩子目前拥有的糖果数量」中的最大值。
然后再遍历求所有孩子的糖果数量和即为答案。
代码:
class Solution:
def candy(self, ratings: List[int]) -> int:
size = len(ratings)
sweets = [1 for _ in range(size)]
for i in range(1, size):
if ratings[i] > ratings[i - 1]:
sweets[i] = sweets[i - 1] + 1
for i in range(size - 2, -1, -1):
if ratings[i] > ratings[i + 1]:
sweets[i] = max(sweets[i], sweets[i + 1] + 1)
res = sum(sweets)
return res
救生艇
题目链接:救生艇
描述:给定一个整数数组 people
代表每个人的体重,其中第 i
个人的体重为 people[i]
。再给定一个整数 limit
,代表每艘船可以承载的最大重量。每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit
。
要求:返回载到每一个人所需的最小船数(保证每个人都能被船载)。
说明:
- 1≤people.length≤5×。
- 1≤people[i]≤limit≤3×。
示例:
- 示例 1:
输入:people = [1,2], limit = 3
输出:1
解释:1 艘船载 (1, 2)
- 示例 2:
输入:people = [3,2,2,1], limit = 3
输出:3
解释:3 艘船分别载 (1, 2), (2) 和 (3)
解题思路:
关于这个问题我们要想怎么让一条船的利益最大化,让船载下尽可能多的人?
让最重的人和最轻的人一起走似乎是一个好办法。
首先我们肯定先要对每个人的体重进行排序,然后计算当下最重的人和最轻的人体重之和并与limit进行比较,如果小于等于,船数加1,除去这两个人计算当下最重的人和最轻的人体重;如果大于,则船数加1,只装最重的人,除去这个人计算当下最重的人继续比较。
相应的代码如下:
class Solution:
def numRescueBoats(self, people: List[int], limit: int) -> int:
people.sort()
size = len(people)
left, right = 0, size - 1
ans = 0
while left < right:
if people[left] + people[right] > limit:
right -= 1
else:
left += 1
right -= 1
ans += 1
if left == right:
ans += 1
return ans
跳跃游戏
题目链接:跳跃游戏
描述:给定一个非负整数数组 nums
,数组中每个元素代表在该位置可以跳跃的最大长度。开始位置位于数组的第一个下标处。
要求:判断是否能够到达最后一个下标。
说明:
- 1≤nums.length≤3×104。
- 0≤nums[i]≤105。
示例:
- 示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再下标 1 跳 3 步到达最后一个下标。
- 示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
解题思路:
不断地更新能到达的最远位置,最后与数组的长度比较
如果我们能通过前面的某个位置 j,到达后面的某个位置 i,则我们一定能到达区间 [j,i] 中所有的点(j≤i)。
而前面的位置 j 肯定也是通过 j 前面的点到达的。所以我们可以通过贪心算法来计算出所能到达的最远位置。具体步骤如下:
- 初始化能到达的最远位置 maxi 为 0。
- 遍历数组
nums
。 - 如果能到达当前位置,即 maxi≤i,并且当前位置 + 当前位置最大跳跃长度 > 能到达的最远位置,即 i+nums[i]>maxi,则更新能到达的最远位置 maxi。
- 遍历完数组,最后比较能到达的最远位置 maxi 和数组最远距离
size - 1
的关系。如果 maxi>=len(nums),则返回True
,否则返回False
。
代码:
class Solution:
def canJump(self, nums: List[int]) -> bool:
size = len(nums)
max_i = 0
for i in range(size):
if max_i >= i and i + nums[i] > max_i:
max_i = i + nums[i]
return max_i >= size - 1
跳跃游戏2
题目链接:跳跃链接
描述:给定一个非负整数数组 nums
,数组中每个元素代表在该位置可以跳跃的最大长度。开始位置为数组的第一个下标处。
要求:计算出到达最后一个下标处的最少的跳跃次数。假设你总能到达数组的最后一个下标处。
说明:
- 1≤nums.length≤104。
- 0≤nums[i]≤1000。
示例:
- 示例 1:
输入:nums = [2,3,1,1,4]
输出:2
解释:跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
解题思路:
对于每一个位置 i
来说,所能跳到的所有位置都可以作为下一个起跳点,为了尽可能使用最少的跳跃次数,所以我们应该使得下一次起跳所能达到的位置尽可能的远。简单来说,就是每次在「可跳范围」内选择可以使下一次跳的更远的位置。这样才能获得最少跳跃次数。具体做法如下:
- 定义几个变量:当前所能达到的最远位置
end
,下一步所能跳到的最远位置max_pos
,最少跳跃次数setps
。 - 遍历数组
nums
的前len(nums) - 1
个元素: - 每次更新第
i
位置下一步所能跳到的最远位置max_pos
。 - 如果索引
i
到达了end
边界,则:更新end
为新的当前位置max_pos
,并令步数setps
加1
。 - 最终返回跳跃次数
steps
。
代码:
class Solution:
def jump(self, nums: List[int]) -> int:
end, max_pos = 0, 0
steps = 0
for i in range(len(nums) - 1):
max_pos = max(max_pos, nums[i] + i)
if i == end:
end = max_pos
steps += 1
return steps
工作原理
-
在循环中迭代
nums
数组的每个元素,除了最后一个元素(因为不需要从最后一个元素再跳跃),使用range(len(nums) - 1)
来遍历。 -
对于每个索引
i
,更新max_pos
为i + nums[i]
和当前max_pos
中的较大值。这表示我们正在检查当前位置i
能否让我们跳得更远。 -
如果当前索引
i
已经等于end
,这意味着我们已经用尽了上一次跳跃所能提供的范围。此时我们需要进行下一次跳跃。因此,我们将end
更新为max_pos
,表示新的跳跃范围,并增加steps
来记录这次跳跃。就证明end已经大于等于数组的长度了,即可以到达最后一个下标处了,steps也不需要再增加了。 -
当循环结束时,我们已经找到了达到最后一个位置所需的最小跳跃次数,因此返回
steps
。
如果最后没走到边界也就是最后的
end,
就证明end已经大于等于数组的长度了,即可以到达最后一个下标处了,steps也不需要再增加了。
用最少数量的箭引爆气球
题目链接:用最少数量的箭引爆气球
描述:在一个坐标系中有许多球形的气球。对于每个气球,给定气球在 x 轴上的开始坐标和结束坐标 (xstart,xend)。
同时,在 x 轴的任意位置都能垂直发出弓箭,假设弓箭发出的坐标就是 x。那么如果有气球满足 xstart≤x≤xend,则该气球就会被引爆,且弓箭可以无限前进,可以将满足上述要求的气球全部引爆。
现在给定一个数组 points
,其中 points[i]=[xstart,xend] 代表每个气球的开始坐标和结束坐标。
要求:返回能引爆所有气球的最小弓箭数。
说明:
- 1≤points.length≤。
- points[i].length==2。
- −≤xstart<xend≤−1。
示例:
- 示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用 2 支箭来爆破:
- 在x = 6 处射出箭,击破气球 [2,8] 和 [1,6]。
- 在x = 11 处发射箭,击破气球 [10,16] 和 [7,12]。
- 示例 2:
输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要 4 支箭
解题思路:
我们使用贪心算法求最小弓箭数,即让每个弓箭利益最大化,那就让用重叠区域的气球只用一根弓箭即可。那么如何判断气球有没有重叠区域呢?
首先肯定先要对数组进行排序,按结束坐标会简单一点,如果按开始坐标排序,出现下面情况不好判断弓箭的位置:
[0..................6]
[1..2]
[4..5]
而按结束坐标,取重叠气球结束坐标的最小值即可为弓箭的位置。
然后判断分界的情况,气球间不想重叠怎么办:
初始情况下,第一支弓箭的坐标为第一个区间的结束位置,然后弓箭数为 1。然后依次遍历每段区间。
如果遇到弓箭坐标小于区间起始位置的情况,说明该弓箭不能引爆该区间对应的气球,需要用新的弓箭来射,所以弓箭数加 1,弓箭坐标也需要更新为新区间的结束位置。
最终返回弓箭数目。
代码:
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
points.sort(key=lambda x:x[1])
res=1
r=points[0][1]
for i in range(1,len(points)):
if points[i][0]>r:
r=points[i][1]
res+=1
return res
卡车上的最大单元数
题目链接:卡车上的最大单元数
描述:现在需要将一些箱子装在一辆卡车上。给定一个二维数组 boxTypes,其中 boxTypes[i]=[numberOfBoxesi,numberOfUnitsPerBoxi]。
numberOfBoxesi 是类型 i 的箱子的数量。numberOfUnitsPerBoxi 是类型 i 的每个箱子可以装载的单元数量。
再给定一个整数 truckSize 表示一辆卡车上可以装载箱子的最大数量。只要箱子数量不超过 truckSize,你就可以选择任意箱子装到卡车上。
要求:返回卡车可以装载的最大单元数量。
说明:
- 1≤boxTypes.length≤1000。
- 1≤numberOfBoxesi,numberOfUnitsPerBoxi≤1000。
- 1≤truckSize≤106。
示例:
- 示例 1:
输入:boxTypes = [[1,3],[2,2],[3,1]], truckSize = 4
输出:8
解释
箱子的情况如下:
- 1 个第一类的箱子,里面含 3 个单元。
- 2 个第二类的箱子,每个里面含 2 个单元。
- 3 个第三类的箱子,每个里面含 1 个单元。
可以选择第一类和第二类的所有箱子,以及第三类的一个箱子。
单元总数 = (1 * 3) + (2 * 2) + (1 * 1) = 8
解题思路:
题目中,一辆卡车上可以装载箱子的最大数量是固定的(truckSize),那么如果想要使卡车上装载的单元数量最大,就应该优先选取装载单元数量多的箱子。
所以,从贪心算法的角度来考虑,我们首先应该按照每个箱子可以装载的单元数量对数组 boxTypes 从大到小排序。然后优先选取装载单元数量多的箱子。
代码:
class Solution:
def maximumUnits(self, boxTypes: List[List[int]], truckSize: int) -> int:
boxTypes.sort(key=lambda x:x[1], reverse=True)
sums=0
if truckSize==0:
return 0
for i in range(len(boxTypes)):
while boxTypes[i][0]>0 and truckSize>0:
truckSize-=1
boxTypes[i][0]-=1
sums+=boxTypes[i][1]
if truckSize==0:
return sums
if truckSize>0 and i == len(boxTypes)-1 and boxTypes[i][0]==0:
return sums
if boxTypes[i][0]==0:
break
这里还要注意的边界条件是当前已经是最后一个箱子,但是卡车还可以装载时,依旧要返回sums
if truckSize>0 and i == len(boxTypes)-1 and boxTypes[i][0]==0:
return sums
如果不加这个,出现上述情况时会返回空值。
我们也可以对其进行简化:
class Solution:
def maximumUnits(self, boxTypes: List[List[int]], truckSize: int) -> int:
boxTypes.sort(key=lambda x:x[1], reverse=True)
res = 0
for box in boxTypes:
if truckSize > box[0]:
res += box[0] * box[1]
truckSize -= box[0]
else:
res += truckSize * box[1]
break
return res
这样就不用逐个判断装载了,因为大的箱子如果能装下就一定会装,所以只需要判断是全装还是部分装就行了,甚至不需要判断truckSize是否大于0,因为等于0后对结果没有影响。
那我们今天的学习就到这里,我们下期再见!