来谈谈贪心算法

前言

以前讲了动态规划,在翻阅资料的时候看到了很多谈论贪心算法的,这两种算法也颇有类似之处,正好最近又作到了有关贪心的题,因此今天写篇文章来谈一谈。python

贪心算法(英语:greedy algorithm),又称贪婪算法,是一种在每一步选择中都采起在当前状态下最好或最优(即最有利)的选择,从而但愿致使结果是最好或最优的算法。
贪心算法在有最优子结构的问题中尤其有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题可以分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。
贪心算法与动态规划的不一样在于它对每一个子问题的解决方案都作出选择,不能回退。动态规划则会保存之前的运算结果,并根据之前的结果对当前进行选择,有回退功能。
贪心法能够解决一些最优化问题,如:求图中的最小生成树、求哈夫曼编码……对于其余问题,贪心法通常不能获得咱们所要求的答案。一旦一个问题能够经过贪心法来解决,那么贪心法通常是解决这个问题的最好办法。因为贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也能够用做辅助算法或者直接解决一些要求结果不特别精确的问题。
——摘自维基百科

动态规划和贪心算法很像,在各类对它们的描述中都有将问题分解为子问题的说法,其实还有分治法也是这种模式。可是动态规划实质上是穷举法,只是会省去重复计算,而贪心算法,正如它的名字,贪心,每次都选择局部的最优解,并不考虑这个局部最优选择对全局的影响。
能够说贪心算法是动态规划的一种特例,也正因为贪心算法只考虑子问题的最优解,能够说,贪心算法实际上能解决的问题有限,它是一个目光短浅的算法,只考虑当下,只有当这种基于局部最优的选择最终能致使总体最优解的情形才能用贪心算法来解决。算法

仍是举个栗子

一块儿来看一下一道leetcode上的题:数组

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。可是,每一个孩子最多只能给一块饼干。对每一个孩子 i ,都有一个胃口值 gi ,这是能让孩子们知足胃口的饼干的最小尺寸;而且每块饼干 j ,都有一个尺寸 sj 。若是 sj >= gi ,咱们能够将这个饼干 j 分配给孩子 i ,这个孩子会获得知足。你的目标是尽量知足越多数量的孩子,并输出这个最大数值。
注意:
你能够假设胃口值为正。
一个小朋友最多只能拥有一块饼干。
来源:力扣(LeetCode)
连接: https://leetcode-cn.com/probl...
著做权归领扣网络全部。商业转载请联系官方受权,非商业转载请注明出处。

是的,这是一位很棒(抠门)的家长,要尽量用少的饼干知足多的孩子。好比如今有三个孩子胃口是[1,2,3],那么哪怕家长手上有一百块尺寸为1的小饼干,也只能知足一个孩子,由于他每一个孩子最多只给一个饼干。
让咱们来想想如何“贪心”呢?
要想最节省饼干,咱们能够把饼干尺寸孩子胃口这两个数据先作一下升序排序,而后每次都用最小的饼干去试试可否知足胃口最小的孩子,这样咱们须要维护两个索引。微信

代码实现:

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        count = 0
        g.sort()
        s.sort()
        gi, si = 0, 0
        while gi < len(g) and si < len(s):
            if s[si] >= g[gi]:
                count += 1
                gi += 1
                si += 1
            elif s[si] < g[gi]:
                si += 1
        return count

当饼干尺寸恰好大于等于孩子胃口,计数+1,两个索引值+1,不然,饼干尺寸列表索引+1,看看更大的那块饼干可否知足当前孩子。
题外话:常常看到有的Python代码中,将某个列表长度值保存到某个变量中,像size = len(alist)这样,事实上len()函数花费的是O(1)常数时间。Python的设计中一切皆对象,列表固然也是对象,当你建立一个列表后,len()实质上只是去提取了这个列表实例的长度属性值而已,并无遍历列表之类的操做。cookie

实践

再来看个题目:网络

在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站须要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
若是你能够绕环路行驶一周,则返回出发时加油站的编号,不然返回 -1。
说明: 
若是题目有解,该答案即为惟一答案。
输入数组均为非空数组,且长度相同。
输入数组中的元素均为非负数。

首先咱们能够想到,有一种状况,是必定不可能跑彻底程的,那就是加油站的油量总和小于路上消耗的总油量时。也就是说,若是sum(gas) < sum(cost),那么就要返回-1
第二点,若是咱们选择一个加油站i为起始点,若是这个加油站所可以得到的油量小于前往下一个加油站所花费的油量,也就是gas[i] < cost[i]的话,说明这个加油站不能作为起点。函数

代码实现:

class Solution:
    def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
        total, curr = 0, 0
        start = 0
        for i in range(len(gas)):
            total += gas[i] - cost[i]
            curr += gas[i] - cost[i]
            if curr < 0:
                start = i + 1
                curr = 0

        return start if total >= 0 else -1

这里用total保存最终的油量,curr表示当前油箱油量,start表示起点,初值都设为0,遍历整个列表,若是在加油站i,gas[i] - cost[i] < 0,那么就选择第i+1个加油站作为起点,最后若是total小于0,返回-1,不然就返回start优化

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

看看缺陷

如下例子来自知乎用户@阮行止:ui

先来看看生活中常常遇到的事吧——假设您是个土豪,身上带了足够的一、五、十、20、50、100元面值的钞票。如今您的目标是凑出某个金额w,须要用到尽可能少的钞票。  依据生活经验,咱们显然能够采起这样的策略:能用100的就尽可能用100的,不然尽可能用50的……依次类推。在这种策略下,666=6×100+1×50+1×10+1×5+1×1,共使用了10张钞票。  这种策略称为“贪心”:假设咱们面对的局面是“须要凑出w”,贪心策略会尽快让w变得更小。能让w少100就尽可能让它少100,这样咱们接下来面对的局面就是凑出w-100。长期的生活经验代表,贪心策略是正确的。  可是,若是咱们换一组钞票的面值,贪心策略就也许不成立了。若是一个奇葩国家的钞票面额分别是一、五、11,那么咱们在凑出15的时候,贪心策略会出错:  15=1×11+4×1 (贪心策略使用了5张钞票)  15=3×5 (正确的策略,只用3张钞票)
做者:阮行止
连接: https://www.zhihu.com/questio...
来源:知乎
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

能够看到,在第一种状况下,使用贪心策略,很快就能得出答案,可是当条件稍微改变,就没法得出正确答案了。贪心算法在这个问题中,每次都选择面额最大的钞票,快速减小了最终要凑出的W的量,可是在例子的特殊状况里,第一次选择最大的面额11的钞票,会致使后面只能选择4张1元钞票,最终获得的解是不正确的。
能够说,动态规划是在暴力枚举的基础上,避免了重复计算,可是每个子问题都被考虑到了,而贪心算法则每次都短视的选择当前最优解而不去考虑剩下的状况。
最后留个思考,试试把这个特殊面额钞票的问题用动态规划解决一下。编码

扫码关注微信公众号:
公众号

相关文章
相关标签/搜索