一. 贪心算法
贪心算法的实质就是,直接解决问题太难,于是寻找一个看起来比较tricky的解法,核心思想是先寻找局部最优解,进而整合起来的结果仍然是整体最优解。因为寻找局部最优解的过程,可以忽略很多情况条件(因为它考虑的是局部,而非整体),因此才显得比较“贪心”。但要注意的是,每一个地方都是局部最优解,最终的全局并不一定是整体的最优解,所以贪心算法只有在特定的题目才可以使用。
那么什么题目才可以使用贪心算法?自然是具体问题具体分析。贪心算法有点像直觉上的不成形算法,关键在于这个局部最优解最后是否能演变成整体最优解,一般可以通过反证法来判断是否能得到整体最优解。接下来,我们通过几道LeetCode的题目来进行操作。
二. LeetCode 455. 分配饼干
455.Assign Cookies (Easy)
1 | Input: grid[1,3], size[1,2,4] |
题目描述:每个孩子都有一个满足度 grid,每个饼干都有一个大小 size,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。
分析:一个饼干只能分配给一个小孩子。这道题,直觉上的思路:优先分配给满足度最小的孩子,并且给一个小孩子分配饼干的时候,应该分配尽量小但又能满足那个小孩子的饼干,留着更大的饼干用于给满足度比较大的孩子。这个思想应该很好理解,其实这就已经是贪心算法,为什么?因为优先给当前满足度最小的孩子分配尽量小的饼干,这就是一个局部最优解 的情况。那么,虽然直觉上是认为这样就是正确答案,也就是这个局部最优解,最后会演化成整体最优解,但如果要专业的证明,如何证明?答案就是开头所说的,反证法。
证明:假设在某次选择中,我们的贪心策略选择:给当前满足度最小的孩子分配第m个饼干,并且第m个饼干是可以满足该孩子的最小饼干。假设这并不一定是全局最优解,也就是存在一种更优的策略,可以给该孩子分配第n个饼干,并且m < n。我们可以发现,经过这一轮的分配,贪心策略可以剩下一个饼干n,而假设的更优策略,留下的是饼干m。又由于m < n,所以在后续的分配中,贪心策略一定能满足更多的孩子。所以不存在比贪心策略更优的策略,即贪心策略是最优策略。
总结:贪心算法看起来确实就是一种直觉上的算法,根据具体问题分析,得出局部最优解,进而获得全局最优解。所以后续的题目直接忽略证明,讲述贪心算法的思路即可。(这道题还用到了双指针)
1 | class Solution { |
三. LeetCode 435. 不重叠的区间个数
\435. Non-overlapping Intervals (Medium)
1 | Input: [ [1,2], [1,2], [1,2] ] |
1 | Input: [ [1,2], [2,3] ] |
题目描述:给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
分析:给数组排序,然后从左到右逐个选择,如果重叠(冲突)就去掉。记录选择的区间个数,最后得到的就是结果。那么这个排序的逻辑是什么?答案是:根据右区间进行排序。试想存在两个区间,[1, 2], [1, 3],应该选择哪个?显然是[1, 2],因为它右区间更小,进而后面能容纳的范围也更大。那么左区间不需要考虑吗?虽然左区间比自己的右区间更小,但并不一定比其他的左区间更小,比如[0, 2], [1, 3]。这时候我们可能会钻牛角尖,这种情况肯定是选[0, 2],但如果前面还有一个区间怎么样?比如: [-2, 1], [0, 2], [1, 3]。实际上“如果前面还有一个区间”,这种假设是不可能存在的,因为我们是从左到右 进行选择的。所以不存在我们先选取了[0, 2],再因为前面还有区间而导致出错的情况。我们只会先选择[-2, 1],然后读到[0, 2]的时候,发现冲突,继续向前。读到[1, 3],没有冲突,于是最后的答案就是2。所以这种排序得到的确实是最优解。
总结:贪心算法最关键的是寻找局部最优解,并且要确保这是否真的为局部最优解。在验证的过程中,我们会使用测试用例,但不能忘记自己算法的规则(比如上面的从左到右,依次选择右区间最小的元素,如果忽略了从左到右,那么可能会在一些测试用例上浪费时间)
1 | class Solution { |
四. LeetCode 452. 投飞镖刺破气球
452.Minimum Number of Arrows to Burst Balloons (Medium)
1 | Input: |
题目描述:气球在一个水平数轴上摆放,可以重叠,飞镖垂直投向坐标轴,使得路径上的气球都被刺破。求解最小的投飞镖次数使所有气球都被刺破。
分析:其实跟上一题很相似,就是选择一定数量的数字,使得这些数字一起覆盖上面的区间。同理,进行排序,从左到右进行覆盖(也就是题目中的投飞镖刺气球)。同样是根据右区间排序,然后从左到右选择右区间的位置进行投飞镖。比如区间[1, 6],到底是在1~6哪个位置投最合适?答案是6,也就是右区间,因为更容易同时刺中后面的气球。如果你又要问“如果前面还有一个[3,4]的区间怎么办,建议回看上一题。。)所以这一题的思路跟上一题是一模一样的,只是处理的逻辑稍微有一点区别。
总结: 跟上一题一样。
1 | class Solution { |
五. LeetCode 406. 根据身高和序号重组队列
406.Queue Reconstruction by Height(Medium)
1 | Input: |
题目描述:一个学生用两个分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 个学生的身高比他高或者和他一样高。
分析:首先,毫无疑问重组的队列是一个一个身高insert进去的,不可能一下全部丢进去,然后再重排。如果重排,那就是暴力法,把所有情况都列出来,毫无疑问,时间复杂度很高,而且不推荐。那么关键就是这个依次添加的每一个学生,如何不受后面的学生影响,或者说,后面的学生如何insert到恰当的位置?这就是每一步都寻求的最优解,也就是局部最优,所以也属于贪心算法的范畴。思考了之后,最不容易受影响的学生,就是最高的学生,因为比它们矮的学生,无论怎么排,都不会影响第二个数字(排在前面比它们高或者一样高的学生数量)。所以排序的第一个规则,h越大,就排在越前。那么如果h相同的情况应该怎么处理?答案是,先考虑k较小的情况,只要把该h,放在队列里的第k个位置,那么前面的学生就不会受到影响。拿这里举例:[7, 0], [6, 1], [7, 1]。[7, 0]先入队,放在队列的第0个位置,此时队列为{7}。然后[7, 1]进队,放在队列的第1个位置,此时队列为{7,7}。然后关键的[6, 1]来了,应该放在哪个位置?就是放在队列的第1个位置,这样队列为{7,6,7}。为什么是第0个位置?看起来很巧妙,很难思考,其实也是局部最优的原因。因为前面的放法都是局部最优,所以后面这样加的位置也是正确解。比如前面放置[7, 0]和[7, 1]的时候,不会怀疑这个放法,因为它前面也是局部最优(初始化当然是最优,只加一个的时候当然也是最优)。所以,关键是:身高h降序,个数k升序。
总结:贪心算法的核心是寻找局部最优,并非找到合适的排序就已经结束,以哪种顺序insert,这些都是要考虑的方面。
1 | class Solution { |
六. LeetCode 121. 买卖股票最大的利益
121.Best Time to Buy and Sell Stock (Easy)
题目描述:一次股票交易包含买入和卖出,只进行一次交易,求最大收益。
分析:假设我们要在第i天出售,此时的最大利润是什么?是在前面(i - 1)天寻找一个最小值,然后用prices[i]减去第i天之前的最小值。这就是每一天的最优解(每一天出售时候的最大利益),全部取max,就是答案。
总结:一开始觉得这题跟贪心算法没联系,其实这题再翻译一下就是,在n天内,可以买卖股票,只进行一次交易,那么最大的利益是多少?里面隐含了一个条件,n天之内。实际上,我们只需要求出每一天出售时候的最大利益,最后就能通过每一天出售的最大利益,求得n天之内买卖的最大利益,也就是局部最优到全局最优。
1 | class Solution { |
七. LeetCode 122.买卖股票的最大利益Ⅱ
122.Best Time to Buy and Sell Stock II (Easy)
题目描述:可以进行多次交易,多次交易之间不能交叉进行,可以进行多次交易。
分析:因为可以多次交易,所以上一题的方法无效。这道题很容易让人想太多,比如到底应该什么时候买,什么时候卖(仿佛就回到了现实的纠结当中)。其实有一个最关键的点:对于 [a, b, c, d],如果有 a <= b <= c <= d ,那么最大收益为 d - a。正确的做法应该是,第一天的时候买进a,第四天的时候以d的价格卖出去,所以中间的b和c都是干扰项,这么想就会把问题复杂化。而实际上,d - a = (d - c) + (c - b) + (b - a),因此当访问到一个 prices[i] 且 prices[i] - prices[i-1] > 0,那么就把 prices[i] - prices[i-1] 添加到收益中。如果是c <= a <= b <= d应该怎么办?同样的: d - a = (d - b) + ( b - a)。一天只能买,或者卖,其实这就是干扰项,你第二天可以以b的价格卖掉,此时利润为(b - a),就当做是,你又以b的价格拿回来了,随时可以出售(当然这个现实做不到)。当后面没有比b更好的情况,你的利润就是(b - a),如果有更好的,那么就是继续上面的d - a = (d - c) + (c - b) + (b - a)。所以核心的关键是,只要prices[i] > prices[i - 1],这一部分的利润就可以赚取。这就是这道题的局部最优。
总结:局部最优解并不容易想,但只要想出来,并且能达到整体最优,那么贪心算法很把问题迅速解决。
1 | class Solution { |
八. LeetCode 605. 种植花朵
605.Can Place Flowers (Easy)
1 | Input: flowerbed = [1,0,0,0,1], n = 1 |
题目描述:flowerbed 数组中 1 表示已经种下了花朵。花朵之间至少需要一个单位的间隔,求解是否能种下 n 朵花。
分析:这道题很显然,只要从左到右种,那么就一定是最优(只要当前的格子符合要求,就可以种)。应该很直观吧,不会有人觉得在某些特定的情况,左边不能种,应该种在稍右的地方吧。这个用反证法也很好证明,过程估计就跟第一题的证明一样,种在最左,右边能遗留的空间会更多。
总结:即使不知道这是贪心算法,也知道是这么回事,但确实这也是局部最优到整体最优的过程。
1 | class Solution { |
九. 总结
只要是找局部最优解,进而演变成整体最优解,那么就是贪心算法。但是贪心算法的适用性其实并不广,所以并不能频繁考虑。可是贪心算法比较像直观上思考的算法,所以只要直观上觉得可行,就该考虑这是不是局部最优,是不是属于贪心算法,进而再进行解题。值得注意的是,局部最优解虽然暂时不用考虑整体,但有些情况下也并不容易找到,如果当前的局部解并不是最优解,最后的结果自然也是错的。总而言之,这是一个在特定情况下很好用的方法。