来谈谈贪心算法
Posted 公子政的宅日常
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了来谈谈贪心算法相关的知识,希望对你有一定的参考价值。
前言
之前讲了动态规划,在翻阅资料的时候看到了不少谈论贪心算法的,这两种算法也很有相似之处,正好最近又做到了有关贪心的题,所以今天写篇文章来谈一谈。
贪心算法(英语:greedy algorithm),又称贪婪算法,是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。
贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。
贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
贪心法可以解决一些最优化问题,如:求图中的最小生成树、求哈夫曼编码……对于其他问题,贪心法一般不能得到我们所要求的答案。一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。由于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。
——摘自维基百科
动态规划和贪心算法很像,在各种对它们的描述中都有将问题分解为子问题的说法,其实还有分治法也是这种模式。但是动态规划实质上是穷举法,只是会省去重复计算,而贪心算法,正如它的名字,贪心,每次都选择局部的最优解,并不考虑这个局部最优选择对全局的影响。
可以说贪心算法是动态规划的一种特例,也正由于贪心算法只考虑子问题的最优解,可以说,贪心算法实际上能解决的问题有限,它是一个目光短浅的算法,只考虑当下,只有当这种基于局部最优的选择最终能导致整体最优解的情形才能用贪心算法来解决。
还是举个栗子
一起来看一下一道leetcode上的题:
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
注意:
你可以假设胃口值为正。
一个小朋友最多只能拥有一块饼干。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/assign-cookies
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
是的,这是一位很棒(抠门)的家长,要尽可能用少的饼干满足多的孩子。比如现在有三个孩子胃口是[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()
实质上只是去提取了这个列表实例的长度属性值而已,并没有遍历列表之类的操作。
实践
再来看个题目:
在一条环路上有 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)
看看缺陷
以下例子来自知乎用户@阮行止:
可以看到,在第一种情况下,使用贪心策略,很快就能得出答案,但是当条件稍微改变,就无法得出正确答案了。贪心算法在这个问题中,每次都选择面额最大的钞票,快速减少了最终要凑出的W
的量,但是在例子的特殊情况里,第一次选择最大的面额11的钞票,会导致后面只能选择4张1元钞票,最终得到的解是不正确的。
可以说,动态规划是在暴力枚举的基础上,避免了重复计算,但是每一个子问题都被考虑到了,而贪心算法则每次都短视的选择当前最优解而不去考虑剩下的情况。
最后留个思考,试试把这个特殊面额钞票的问题用动态规划解决一下。
以上是关于来谈谈贪心算法的主要内容,如果未能解决你的问题,请参考以下文章