算法刷题重温(五):无套路,练贪心

1. 写在前面

今天开始复习算法的贪心专题, 这个专题比较可悲的是并没有像前面树或者回溯那边,有几个完整的框架,会默写了之后就差不多能解题, 贪心这块就没有这么爽了,没有那种写好的框架可以套,说白了讲, 这可能算是一种辅助思维,只能凭着我们平时积累的“贪心”思维(常识)去摸索着解题了哈哈, 所以只能把常用贪心的题目整理到一块, 总体上看看风格并熟悉,这样到后面解题的时候, 只要有感觉就试试贪心。

贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择, 从而希望导致结果是全局最好或者最优的算法。解题思路一般是:

  • 问题分解为若干子问题
  • 找出适合的贪心策略
  • 求解每个子问题的最优解
  • 将局部最优解堆叠成全局最优

由于这里的题目并没有固定的套路,所以直接从题目中体会这种贪心的思维了。贪心的两个本质:局部最优和全局最优, 是否前者能够推出后者。 所以遇到问题,如果发现不是明显的框架思维的题目,而又涉及到优化的时候, 那么就可以分析局部最优,全局最优,如果再发现前者能推出后者, 试试贪心再说 😉

PS: 下面的第二部分是我目前做过的题目整理(思路和代码),第三部分是第二部分的题目汇总,只有题目,这个方便复习用,如果是为了回顾思路和复习,直接看第三部分,如果想不起来思路和代码了,再瞟一眼第二部分哈哈,尽量不要从头开始读,那样会非常难受,并且失去了刷题复习的趣味性了!

2. 题目思路整理

这里的整理,是把题目的思路和代码走了一遍, 如果想复习这块, 直接看下面的总结, 那里是只有题目,可以根据题目想想思路哈。

2.1 贪心常识题

  • LeetCode455: 分发饼干: 贪心的策略是大尺寸的饼干应该满足胃口大的孩子才能物尽其用。所以局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸,全局最优是喂饱尽可能多的小孩,感觉局部最优能推出全局最优,并想不出反例,试试贪心。

    在这里插入图片描述
  • LeetCode1005: K次取反后最大化数组和: 这个题一开始没有想到贪心的思路,直接暴力解,就是每次找最小的元素进行取反, 看了Carl大哥的题解之后,才发现原来这里面还有贪心的方式,着实扩展了一波脑洞。贪心一: 找绝大值大的负数,然后取反变为整数, 贪心二: 如果都是正数了, 找数值最小的正数反转。 全局最优就是整个数组和最大, 而局部最优就是保证必须反转的情况下,反转当前数值的效果会最好。 这里直接上贪心的方法了:
    在这里插入图片描述
  • LeetCode860: 柠檬水找零: 这个题目相对来说比较简单, 模拟一下找零的过程即可, 只需要维护3种金额的钞票: 5元, 10元, 20元,有三种情况:①账单是5, 直接收下 ②账单是10, 那么就消耗一个5, 增加一个10 ③账单是20, 那么优先消耗一个10, 一个5, 如果没有10, 那么就消耗3个5。这里面的贪心思路就是账单是20的时候, 如果有10先消耗10, 没有10再用5来替。
    在这里插入图片描述
  • 剑指offer14-I: 剪绳子I: 这个题的贪心解法是当 n > = 5 n>=5 n>=5的时候,尽可能将绳子剪成长度为3的段, 当 n < 5 n<5 n<5的时候, 剩下的不再剪了。这里实现的方式比较巧妙。代码如下:
    在这里插入图片描述
  • 剑指offer14-II: 剪绳子II: 这个题对于python并没有太大影响,无非就是最后的时候取模一下,因为python里面无溢出一说, 但其他语言的话会受到溢出的影响。
    在这里插入图片描述
    下面说原因,也就是为啥 n > = 5 n>=5 n>=5的时候, 尽可能剪成长度为3的绳子会使得乘积大, 首先根据前面的不等式,不难证明 2 ( n − 2 ) > n , 3 ( n − 3 ) > n 2(n-2)>n, 3(n-3)>n 2(n2)>n,3(n3)>n。也就是说绳子剩下的长度大于等于5的时候,我们把它剪成3或者2的绳子段比不剪开要好。而 3 ( n − 3 ) > = 2 ( n − 2 ) 3(n-3)>=2(n-2) 3(n3)>=2(n2),所以当 n > = 5 n>=5 n>=5的时候,尽量的剪成3的绳子短乘积会更大。但是当 n < 5 n<5 n<5了,此时不剪要比剪开好。

2.2 贪心中等题,思路开始巧妙

  • LeetCode322: 零钱兑换: 这个题目的标准解法是动态规划, 这里贪心并不是好的解法,因为这个题目用贪心很容易忽略一些情况。贪心在这里可以快速帮助找一个局部最优的情况,提过了一个很好的剪枝策略。 下面说思路:

    1. 想要总硬币总数最少, 那么就需要优先使用最大的硬币开始换, 所以coins需要先进行排序
    2. 从最大硬币开始, 进行兑换的时候, 可以直接考虑用上全部的最大硬币, 如果发现都用上之后, 后面的硬币凑不到最终的amount了, 这时候, 考虑减少当前最大硬币的数量。

    代码逻辑是这样:

    1. 给定coins, amount, 当前硬币的索引, 累积的数量统计情况之后, 进行递归
    2. 递归结束条件: amount=0, 这时候说明找到了一种兑换方案, 但千万要注意,此时不一定是最优的,我们需要保留最优
    3. 越界情况: 如果发现了索引越界了,说明已经遍历完了所有情况, 返回
    4. 小剪枝: 这个起的作用不是很大, 但确实可以加速一点点, 就是发现当前的amount<coins的最后一个的时候,说明最小的硬币都比amount大,这时候没必要再往下进行了,直接没法兑换
    5. 下面就是兑换过程:
      1. 首先,从能兑换的最大数量开始, 到当前硬币不能兑换为止
      2. 大剪枝: 如果发现当前累积数量+能兑换的当前硬币数量大于当前的最优值时, 这时候就没必要再减少当前硬币数量看后面的结果了,因为此时已经不是最优, 如果再减少当前大硬币数量, 那就需要更多小值硬币的数量,更不可能是最优。
      3. 进行当前硬币兑换, 然后往下走
        在这里插入图片描述
  • LeetCode376: 摆动序列: 这个题目也可以采用贪心的思路, 首先还是先找局部极值点, 局部极值点的判断就是要么同时大于相邻两个数,要么同时小于相邻两个数。 整体最优, 整个序列有最多的局部极值点,局部最优推出全局最优,且找不到反例, 试试贪心, 拿个例子来看:
    在这里插入图片描述
    找极值点这里有个比较好的技巧就是从现在看过去, 当前这个 i i i位置,其实是判断的 i − 1 i-1 i1这个点是不是一个极值点,这一点要注意。下面的代码我连极值点也顺便记录了下来:初识的res=1是默认了最右边那个点是峰值, 第一个点是不是峰值需要借助第二个点, 第二个点需要借助第三个点,依次类推,倒数第二个点需要借助倒数第一个点,而最后一个点默认是峰值。 上面图里面有个小错误,就是最后极值点8后面不应该有绿色的那个点了,这时候退出循环了。

    在这里插入图片描述
    我发现在笔试或者面试里面,这种找局部峰值(峰和谷)的题目还是非常喜欢考的,我这两天参加了腾讯的一个笔试题,是这么说的,给定一个数组, 如果对于某个位置 i i i点的元素,满足 A [ i − n ] < A [ i − n + 1 ] < . . . < A [ i − 1 ] < A [ i ] > A [ i + 1 ] > A [ i + 2 ] > . . . A [ i + n ] A[i-n]<A[i-n+1]<...<A[i-1]<A[i]>A[i+1]>A[i+2]>...A[i+n] A[in]<A[in+1]<...<A[i1]<A[i]>A[i+1]>A[i+2]>...A[i+n], 则这样的一个序列为一个峰值序列。 然后让在一个给定数组里面,找到最长的峰值序列,返回长度。比如,[2,1,5,7, 6,3, 4], 这里的峰值序列是[1,5,7,6,3],长度是5。

    这个题在现场笔试的时候,我是没有做出来的, 第一个是时间太短,当时只有5分钟考虑这个题的时间,我当时想即使有思路也写不完了,第二个是当时那种情况,确实没想到思路。 晚上回来之后,想了想,这个题目其实和上面这个是非常像的。这个峰值,也是在找极值点。 首先把序列的极大值和极小值的位置找出来。然后遍历,如果是极大值了,那相邻两个极小值之间的距离,保留最大的就是最长的峰值序列。所以我尝试根据上面的写了一款代码。当然没有机会运行正确与否了。

    但是我发现给定的序列里面找极值问题是一个非常重要的点。下面再看一个真实考过的面试题。

  • 面试题10.11: 峰与谷: 这个题目就是把一个数组变成峰谷交替出现的形式。 两个思路比较好,第一个是先把数组排序,然后从首部和尾部交替取数,最后就会成为一个峰-谷-峰的序列。 当然这个题目也可以使用局部贪心。思路是这样的,一开始从峰-谷-峰和谷-峰-谷中选定一种模式,比如谷-峰-谷。 然后选定了第一个元素作为谷, 从第二个开始,本来这个位置应该是峰,如果发现它小于前面的元素了,和前面的元素立即交换,进行局部调整。 也就是峰的位置必须要大于等于前面元素,一旦发现比前面的小了,就交换。 谷的位置必须小于等于前面元素,一旦比前面大了, 交换。这样从局部调整可以推到全局,试试贪心。

    在这里插入图片描述

  • LeetCode738: 单调递增的数字: 这个第一眼想到的就是暴力解法,但不幸超时了, 看了题解之后才发现了贪心的巧妙之处, 贪心的思路真的是不好想, 即使想了也不知道咋写,所以感觉这就是贪心这块的难处。 这个题的思路是这样, 既然找单调递增系列的最大值, 那么局部最优就是先保证两位数里面能找到最大的单调增数,这个就是如果发现当前两位数是个递减的, 则前面数减1,后面数变成9就是最大的单调增。这也是解决这个题目的核心。因为全局最优是整体上小于等于N的最大单调增,局部的符合任意两位数上的最大单调增。 局部能推出全局,用贪心。 思路就是从后往前遍历给出的N, 如果发现前一位比后一位大,前一位减1,标记出后一位的位置(这里不能直接改9),因为多位数的情况,如果中间发现前一位比后面这个大,需要前一位减1,后面的都改成9。所以这里必须先记录改9的最初位置,后面再来一个循环,从这个位置之后所有数改成9即可。代码如下:
    在这里插入图片描述
    这里顺便复习了下python的map和reduce语法,这些其实都是python内置的非常好用的函数。

  • LeetCode53: 最大子序列和:这里贪心思路关键是明白在哪里贪心了,这关系到找计算的起点,如果 -2 1 在一起,计算起点的时候,一定是从1开始计算,因为负数只会拉低总和,这就是贪心贪的地方! 所以局部最优,就是当前“连续和”为负数的时候立刻放弃, 从下一个元素重新计算“连续和”, 因为负数加上下一个元素,“连续和”只会越来越小。 全局最优: 最大的连续和。 在局部最优的时候,只要记录最大的连续和,就能推出全局最优, 试试贪心。这篇题解写的很好。这里的关键:不能让“连续和”为负数的时候加上下一个元素,而不是不让“连续和”加上一个负数

    在这里插入图片描述

  • LeetCode674:最长连续递增子序列: 这个题目贪心的思路和上面这个类似,只不过那个是最大子序列和,而这里是记录最大连续子序列的长度, 这里用一个cou计数, 如果后面的元素大于前一个元素, cou加1, 如果发现cou大于最大结果,最大结果更新, 如果后面的元素小于等于前一个元素,cou等于1重新计数。

    在这里插入图片描述

2.3 贪心股票问题

  • LeetCode121: 买卖股票的最佳时机: 这个题贪心的思路就是因为股票就买卖一次, 那么很自然的就是左边取最小值, 右边取最大值,那么得到的差值就是最大利润了, 所以从左往右遍历, low记录最低价格,而根据这个最低价格边更新最大利润。代码如下:

    在这里插入图片描述

  • LeetCode122: 买卖股票的最佳时机II: 这个题目是可以采用贪心非常巧妙的A掉的一道题目, 贪心算法的直觉:由于不限制交易次数,只要今天股价比昨天高,就交易。核心就是把利润分解为每天为单位的维度。比如一只股票,第0天买入, 第三天卖出, 那么利润为:res = (prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])= prices[3] - prices[0], 那么我们就可以贪了,即只收集正利润的区间。 局部最优是收集每天的正利润, 全局最优,求得最大利润。 局部推出全局, 试试贪心。当然要注意,贪心的计算方式只能计算,并不是真正的交易过程。

    在这里插入图片描述

  • LeetCode714: 买卖股票的最佳时机含手续费: 这个题目贪心的思路可能有点不太好想了,和上面的这个不同是每一单都需要考虑加不加手续费了,这时候就不能只要有利润就卖的策略了,而是尽量最低价格买入,有利润的时候就卖。具体看代码:

    在这里插入图片描述
    这些股票买卖的题目,后面会有动态规划串起来,贪心的思路可能都不太好想。标准常规的做法还是动态规划,在那里还有更多的股票买卖场景。

2.4 两个维度权衡问题

  • LeetCode135: 分发糖果: 这个题算是贪心策略的考察题目, 感觉也是比较有意思的一个题目了,生活场景的贴切模拟。切记一定不要一下子就考虑两边, 直接考虑两边这个题目根本无从下手,毕竟两两相互关联着,能做的就是先保证一边,再保证一边。
    先考虑右边孩子比左边孩子评分更高的情况,如果高了, 就给右边的孩子提升糖果个数。 这时候局部最优是右边的孩子评分高,就增加糖果个数,全局最优是评分高的右孩子会比左孩子的糖果个数多。然后再考虑左边孩子比右边孩子评分更高的情况, 从后往前遍历, 如果发现左边孩子的评分高, 左边孩子的糖果数增加, 这样局部最优就是左边孩子评分高就增加糖果数,全局最优是保证评分高的左孩子会比右孩子的糖果数多。
    通过上面这两次,就可以保证评分更高的孩子比两侧的邻位孩子都能获得更多的糖果了。具体过程就是先初始化一个糖果分发数组, 默认每个孩子都分1个糖果。 然后从左往右遍历,如果发现后面的孩子的评分比前一个孩子的高, 后一个孩子的糖果数要是前一个孩子的糖果数加1。 这样遍历完毕之后才能保证评分高的右孩子的糖果都比左边孩子多。 然后从右往左遍历, 如果发现前一个孩子的分数比后一个孩子的分数多, 这时候, 前一个孩子的糖果数要是原先的糖果数和后一个孩子糖果数加1的最大值, 这个算是个小坑, 因为这时候如果本身前面孩子的糖果数就多,就不用管了, 如果糖果数少的话才更新。 所以max操作可以实现这个目的。 最终candyassign数组就是分配的每个孩子的糖果数量了
    在这里插入图片描述
  • LeetCode406: 根据身高重建队列: 这个题目和上面这个有点像,也是两个维度上面的权衡问题, 这种情况下,一定要先确定一个,再去确定另一个,而不是一块考虑。 这个题目先确定下身高来,按照身高从大到小排列,身高相同, 则k小的在前,然后建立数组,按照k所在下标重新插入即可。拿Carl大神画的示意图
    在这里插入图片描述
    局部最优是优先按照身高高的people的k来插入,插入操作过后的people满足队列属性, 全局最优是最后都做完插入操作,整个队列满足题目队列属性。依照这个思路,可以写代码, 这里踩了个坑,就是python的二维列表排序这里:
    在这里插入图片描述
    这里值的记录一个python的知识点,就是python的list列表, 是“长度可变的数组”, 但这个千万不要和C++的链表这种东西混淆了,这里看到说如果想再高效些,可以把上面的queue建立成链表的形式, 所以就产生好奇python的动态改变数组到底是咋实现的,于是就查了查,发现python列表的元素操作原来是这么玩的,学习了,参考这篇文章。这里好奇,写了个链表的操作,反而会更慢, 看来python列表的底层确实高效。 对比下上面:

    在这里插入图片描述
    这里再记录一个套路性的东西,在后面题解中发现的:一般这种数对,还涉及排序的,根据第一个元素正向排序,根据第二个元素反向排序,或者根据第一个元素反向排序,根据第二个元素正向排序,往往能够简化解题过程。 涉及到python的知识: python二维列表的排序,lambda或者是itemgetter

2.5 贪心解决区间问题

  • LeetCode55: 跳跃游戏: 这个题目贪心的地方在于每次会更新在当前能够跳到的最大范围, 如果这个范围覆盖了终点,那么就说明能跳到终点。 局部最优就是对于当前的位置,找到能跳到的最大范围。 全局最优就是最远的范围,看看这个能不能盖过终点。 这里用了一种更加快速的方式,就是如果跳的最大范围到不了当前位置,直接结束掉。

    在这里插入图片描述

  • LeetCode45: 跳跃游戏II: 这个是总是能够到大数组的最后一个位置, 要求最小的跳跃数了。 上面那个是判断能不能跳过去。两个还是有区别的。这个依然是贪心的经典题目, 采用贪心的思想,与上面那个不一样的地方就是, 这个贪心的时候,不是关注的最大覆盖范围, 而是最少跳跃次数。也就是这个在遍历的时候,要记录一下每个位置上所能够到达的最远距离。 遍历的时候,如果发现到了这样的一个边界,就说明需要跳一下子了。
    在这里插入图片描述

  • LeetCode452: 用最少数量的箭引爆气球: 这个题目的贪心策略就是让一支箭能够射尽量多的气球, 如果想达到这个策略,就需要找出有重叠的区间的气球, 一起射。 所以局部最优就是位于重叠区间的气球一起射,全局最优是用箭数最少。 具体操作就是按照气球的终点从小到大排序, 这个东西代表着箭的射程。接下来遍历每个气球,如果发现当前气球的起始点大于了之前箭的射程(说明不重叠了), 此时需要另一支箭,同时射程要更新为当前气球的终止点。
    在这里插入图片描述
    这个题也可以按照起始点从小到大排序, 那判断重叠区间的时候和更新箭的射程的时候就变了方式了,具体看题解, 这里还是贴出来吧, 发现后面的合并区间会用到这种思路:
    在这里插入图片描述

  • LeetCode435: 无重叠区间: 如果上一个题目有感觉了之后, 这个题目竟然能和上面题目一样的代码就可以解。 为啥呢? 因为这两个题目都在判断找不重复区间, 只不过上面是找到不重复区间然后用箭来统计个数,而这里是找到重复区间个数而已,而重复区间的个数正是总的区间个数减去不重复区间的个数。所以理解了这一点,思路就出来了,就是找不重复区间的个数。 而找不重复区间的套路就是先按照终止点从小到大排序,然后遍历每个区间,如果发现当前的起始点大于了之前的终止点,说明区间开始不重复了, 此时更新新的终止点,同时不重复区间个数加1。这个找不重复区间的思路要铭记。所以上个题的代码直接拿过来,还要注意一点,这里的不重复区间和上面不一样的是边界条件得改下,这里有等号了
    在这里插入图片描述
    Carl大佬总结了四个难点,感觉挺不错的:

    1. 一看题需要感觉排序,但究竟怎么排序, 按起始点排还是终止点排?
    2. 排完序之后如何遍历,判断? 一二步是互相影响的其实,排序方式不同,遍历方式也会不同
    3. 直接求重复区间是复杂的, 转而求最大非重复区间,其实就是侧面求最小重复区间,这个转换思路要会,这个思路不仅适用于这个题
    4. 求最大非重复区间个数时,需要分割点来标记
  • LeetCode56: 合并区间: 上面这三个题目总感觉会有些一样, 合并区间这里又是涉及到了找重复区间,并且找到了之后要进行合并。而上面的那种是找不重复的区间。如果统计个数的话,这俩思维是可以进行转换的,只不过这里需要找到重复的且进行合并,就不能用上面那种逆向思路了。 而是采用射气球的第二种思路,毕竟涉及到合并重复区间,得知道当前两个重复区间的最小最大范围。 我这里的思路是这样的, 首先按照起始点从小到大排序,然后遍历,对于当前的区间,如果起始点小于等于前面区间的终止点了,说明重复了区间,此时更新当前区间起始点和终止点(合并完了相当于)。否则说明当前区间与前面的不重叠,把前面的这个保存到最终结果。由于这种思路是没法进行最后一个区间判断或者保存的,所以我这里使用了一种增加额外边界的方式,也就是在最后加了一个肯定和前面无法合并的区间,目的是让原来的最后一个区间可判断。目前题解里面还真没这么玩的哈哈。但感觉好理解且代码简洁:

    在这里插入图片描述
    所以涉及到重叠区间的这种题目,我冥冥之中发现了一点规律,就是数组肯定是要排序,到底是按照终止点排还是起始点排,后面遍历处理的逻辑会不一样,但一般会用到最小最大值的更新,所以上面这三个题目,尤其是射气球的那两种思路最好会默写。

  • LeetCode763: 划分字母区间: 这个题目没有思路, 又是被未来吓住了,因为这个题目里面说同一个字母最多出现在一个片段,所以总是想着从前面划分的话又看不到后面怎么保证这个最多出现一个片段的,而为啥不先遍历统计一遍呢?所以现在刷题总有种“偷懒”的感觉,这样不太好。 再说这个题目, 看了题解之后才发现思路原来这么巧妙,还幸亏看了题解,否则我真的想不到这种思路,所以另一个体会就是不能在一个题上耗费时间太长,如果发现想了好久也不会,那就看题解进行学习,不一定是咱笨,而是知识储备上不足哈哈。那这个题目巧妙在了那里呢? 就是分割点那里。 这里的思路是先遍历一遍,找到每个字母出现的最远距离(这个也是第一次见), 有了每个字母出现的最远距离之后, 再遍历一遍,遍历过程中找到前面字母最大的边界,如果发现到了这个边界了,就找到了分割点,因为前面所有字母的最大边界就是这了,肯定这些字母都出现在了前面这段。 这时候记录这段长度, 往后继续遍历。 拿Carl大神的图一目了然:出自这里

    在这里插入图片描述
    有了这个思路, 代码就可以写了, python这里的hash表或者建立字典,可以使用defaultdict
    在这里插入图片描述
    这个题目的思路非常巧妙,学习到了统计字符出现的最远位置方式, defaultdict统计未知字符的个数或者最远距离非常适合。

2.6 贪心模拟题

  • LeetCode874: 模拟机器人行走: 这个准确的说算个模拟题,一定要注意题目中的: 从原点到机器人所有经过的路径点的最大欧式距离的平方,这里可不是机器人终点到原点哟!所以在机器人走的过程中,途径的每一个点,都要算一下与原点的最大欧式距离的平方,然后保存最大值。这个题的关键是机器人怎么走?

    • 方向
      这个很重要, 也就是当前机器人是沿着哪个方向走, 这里可以把四个方向都设定出来,分别是[0,1], [1,0], [0,-1], [-1,0]分别代表着上,右,下,左。然后把这个放在数组中[[0,1], [1,0], [0,-1], [-1,0]], 初始方向朝上,也就是index=0

    • 左转和右转又如何去改变上面的方向?
      右转, 是遇到了-1, 此时index加1就是接下来的朝向, 不要越界
      左转, 是遇到了-2, 此时index加3就是接下来的朝向,不要越界

    • 模拟走
      搞定了方向之后,就可以根据给定的步长一步步的走了, 如果在走的过程中遇到了障碍点,那么停住,等待下一个命令,否则计算离远点的欧式距离,更新最大值

      在这里插入图片描述

  • LeetCode134: 加油站: 这个题我一开始A掉的思路是暴力, 当时确实没有想到应该如何贪心。 暴力的思路就比较简单了,直接遍历每个位置作为初始位置, 对于每个初始位置, 尝试去走走看, 如果能够走回初始点,就返回,否则,停掉找下一个。 那么如何判断能不能走回初始点呢? 这里我用steps控制步子, 每一步都记录所剩的油, 如果发现油不够到下一站了,也就是当前的油不够当前的消耗, 就停掉。

    在这里插入图片描述
    这里得会模拟这个转圈的过程, 外层用for,内层用while也是有原因的,感觉while就适合模拟这种走走看,不知道啥时候停的转圈场景。这个面试有考的, 所以当一点思路也没有的时候,暴力是最好的方法了, 先A掉再说。 当然,看了题解之后,发现这个题目可以使用贪心,有些巧妙了, 看了好几个大佬说的, 还是感觉自己创的好理解哈哈。 当然不一定普适所有人。下面主要写写这种思路。 要理解贪心的思路,首先要理解两点:

    1. 当前加油站能到下一个加油站的前提: 当前的油箱里的油还有剩余, 因为要知道这个题目里面给定的两个数组, 其实这个索引对应不是当前对当前, 比如第 i i i个加油站的时候, gas[i]表示在当前加油站能加多少油, 而cos[i]表示在当前加油站到下一加油站所耗费的油。所以从当前能到下一个加油站的前提就是当前的rest[i]要大于等于0才行。
    2. 全局的角度来看, 如果想从一个加油站开始, 经过途中不停的加油,放油过程, 再回到该加油站, 比如要保证这个途中的总油量剩余始终大于或者等于0, 否则一定会在中间某个站停掉。 原因就是因为上面那个1,因为小于0了之后,也就是油箱里的油没了,根本到不了下一站。

    所以这样,我们就可以采用贪心的思路,遍历所有的加油站,找到那个累积总油量达到最小的加油站, 让这个加油站的累积总油量大于或者等于0, 这时候就保证了这个途中的所有总油量剩余大于等于0, 这就说明只要从它下面的那个加油站出发,就一定能走一圈回到下面的那个加油站, 这句话要理解,因为在当前加油站总累计油量最小,但是还大于或者等于0, 说明能够到下一个加油站。下一个加油站的位置即我们所求。 这里面的贪心策略, 全局最优,其实就是找那个累积总油量达到最小的加油站,因为找到这个就找到了答案。 而局部最优就是每走个加油站,计算下累积总油量, 如果发现比之前的小,就更新下(找当前的最小), 这样由局部最优可以找到全局最优,就可以试试贪心了。为了说明上面的这个思路,这里还画了两个图加以理解上面的过程,参考了后面一个题解
    在这里插入图片描述
    就会发现,从不同的加油站出发画这个累积图像,会得到不同累积rest的图像,但是这里的不同是位置,而不是形状上, 也就是不同的加油站出发画图,只会影响上面图线的上下位置,而不会改变形状。这是为啥? 有没有发现, 这个累积的过程中, 相邻两个rest的差值是不会变得, 而差值不变,斜率就不会变。无非就是上下平移罢了, 不信? 画画图就了然了。所以根据这个思路,可以写一版更高效的贪心代码:

    在这里插入图片描述
    如果能理解上面的思路, 代码部分应该很容易理解了。当然我发现这个题不同的人有不同的贪心理解,Carl大神给出的贪心思路是另外一种, 也非常的巧妙,就是遍历的过程中去找最优。 这里还是那个思路: 如果想在当前加油站跑完全程,必须保证当前位置累积油量大于等于0, 而一旦小于0了,说明此位置不能作为起始点, 往下一个找。
    在这里插入图片描述
    所以可以体会一下这两种思路,一种是从全局的角度去找最低点,而另一种是我不去管最低点,而是只需要管当前加油站是否能到下一个加油站, 能到的前提就是当前累计油量大于等于0。如果这样的加油站存在,一定可以符合上面的条件,如果在遍历过程中,发现不符合, 往后接着找。但总感觉这个思路怪怪的,不如第二种好记, 第二种图形一结合,很容易就记住了其实。

2.7 贪心典型交叉类问题

  • LeetCode968: 监控二叉树:这个题目如果想要放置的摄像头的个数最少,一定不要把摄像头放到叶子上,而是放到叶子节点的父节点,这样一下能监控三层。 所以局部最优是让叶子节点的父节点安摄像头, 使得摄像头最少。 整体最优, 全部的摄像头数量最少。所以操作思路: 从低向上, 先给叶子节点的父亲放摄像头, 然后隔两个节点放一个摄像头,直到二叉树头结点。从低向上, 可以使用二叉树的后序遍历, 这里的难点就是当前节点的状态判断,是有摄像头还是覆盖还是无覆盖? 每个节点是这三种状态选择。 既然是后序递归,这里分析递归的四要素

    1. 终止条件: 遇到了空节点, 此时返回有覆盖的情况
    2. 先递归左右子树
    3. 单层的处理逻辑:
      1. 当前节点的左孩子和右孩子都有覆盖, 此时中间节点应该是无覆盖的状态
      2. 左右节点至少有一个无覆盖,此时父节点应该放摄像头
      3. 左右节点至少有一个有摄像头,此时父节点是有覆盖状态
      4. 头结点没有覆盖,此时应该再加一个摄像头

    这个hard级别的是真hard啊,名副其实。 具体的思路还是看这个题解,我是跟着大佬的题解翻译成了python而已:

    在这里插入图片描述

3. 小总

贪心这边确实发现没有固定的套路了,只能通过题目修炼内功了, 上面的题目进行汇总如下, 方便后面的复习想思路用, 本次复习用了10天的时间, 复习了大约20道题目,分类整理如下:

贪心常识题:

贪心中等题, 思路开始巧妙:

贪心股票问题:

两个维度权衡问题:

贪心解决区间问题:

贪心模拟题:

贪心典型交叉类难题:

关于贪心这块,还是目前没有找到好的套路,只能通过多练几遍上面的题目,培养一种直觉了,一遍肯定是不够的,都是一些非常巧妙的思路和题目,我得再回味一遍了,然后开始DFS和BFS 😉

这里也推荐一个LeetCode刷题攻略,也是我此次复习跟着走的项目,简直太强了,后面就跟紧大佬的步伐,好好刷题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值