贪心算法其实更准确的表述应该是一种算法思想。它的应用非常广泛,比如:霍夫曼编码,Prim,Kruskal 最小生成树以及 Dijkstra 单源最短路径算法。如果证明贪心算法的可行性需要复杂的数学推导,但是通过简单的举例演示更容易让我们理解这种算法思想,有助于我们在实际中运用。
理解“贪心算法”
如果有一个 100 kg的袋子,只允许装 5 种豆子,如何合理的在装满袋子的前提下,让袋子的总价值最大。
豆子的质量以及价格如下表:
| 物品 | 质量(kg) | 价格(元) |
| :——–: | :——–:| :–: |
| 黄豆| 100 | 100 |
| 绿豆| 30 | 90 |
| 红豆| 60 | 120|
| 黑豆| 20 | 80|
| 青豆| 50 | 75|
首先我们会按照豆子的单价由高到低排序:黑豆,绿豆,红豆,青豆,黄豆。之后,我们肯定会先装完价值最大的,然后装价值次之,直到袋子装满。那么,共装黑豆 20kg,绿豆 30kg,红豆 50kg。
这个问题的解决,就是运用了贪心算法。借助该问题,分析贪心算法的分析过程:
第一步:看到类似的问题,优先考虑贪心算法。那么这种问题的特征就是:拥有限制值和期望值。比如上面的问题,限制值就是质量不能大于 100kg,期望值就是希望袋子所装物品最后的价值最大。
第二步:尝试运用贪心算法是否能解决这个问题。在每次选择的时候,选取相同限制值对贡献值最大的数据。例如:相同质量下的豆子,我们当然选择单价更高的豆子。
第三步:举几个例子验证贪心算法的解是否是最优的。如果严格的证明贪心算法是是最优解,则需要复杂严格的推理过程。但是在大部分情况下,贪心算法的解都是显而易见的最优解。完全可以选择其他方法与贪心算法对比,优劣的比较是十分明显的。
如上图,王老师举了一个例子。在加权图中,我们想找到一条从 S 到 T 的权重最小的路径。按照贪心算法,我们从 S 点出发每一步都会选择权重最小的边,最终的路径就是 S-A-E-T,权重之和为:9。但是,实际上 A-B-D-T 这条路径的权重之和为:6。
为什么贪心算法到了这里失效了呢?原因很简单,该过程算法后续执行的每一步受到前一步的影响很大。也就是说,该算法执行的过程中,每个过程并不是独立不受影响的,这就导致了贪心算法的失效。贪心算法工作的前提就是,前一步的选择不会影响到后一步的决定。
贪心算法的举例
1:分糖果
有 m 个糖果,大小不一,从小到大排列是: s1,s2,s3 - - - sm。把这 m 个糖果分给 n 个小孩,这 n 个小孩对糖果大小的渴求度从小到大是:q1,q2,q3 - - - qn。并且:n 大于 m,及小孩人数大于糖果数量。
如何将糖果分配给这些小孩,并且尽量满足更多的人数。
限制值就是糖果的数量 m ,期望值就是满足小孩的人数。在这个过程中,我们优先选择最小的糖,然后用来满足渴望度最小的小孩。也就是每次都从剩下的小孩中选出对糖渴望度最小的小孩,从剩下的糖块中选择满足他渴望的最小糖块。这个过程就保证了尽可能满足更多的小孩。
2:钱币找零
我们有 1,2,5,10,20,50,100 面值的纸币,现在要找零 k 元,希望用最少张纸币满足。
限制值就是找零的 k 元,期望值就是希望用最少的纸张。为了用最少的纸张,我们肯定会先紧着最大值面额的纸币凑够 k 元。如果不够,就用更小一点面额的纸币凑数。
3:区间覆盖
假设有 n 个区间,区间分别是 [l1,r1],[l2,r2],[l3,r3] - - - [ln,rn]。然后,从这部分区间中选择部分区间,区间的首位没有交叉重合,最多选取多少区间。
限制值区间首位不能交叉重合,期望值就是选取最多的区间。这类问题经常运用到任务调度,教师排课等等问题上。
首先这些区间中选出最左端的点 lmin,最右端的点 rmax,然后构建区间[lmin,rmax]。然后我们从这些区间中选出不想交的区间将[lmin,rmax]区间覆盖上,最后就完成了最多区间选取的问题的求解。
在选择区间的时候,左端的点不选与前一个区间重合的点,右端的点选距离左端最近的点。这样就可以让剩下的未覆盖区间尽量大,尽量容纳更多的区间。
总结
贪心算法最难的就是如何将问题抽象成贪心算法模型。只要这一步完成了,后续的实现就是水到渠成的事情了。所以,要想深刻理解贪心算法,还需要不断的实践和总结,这样才能熟练运用。
本文创作灵感来源于 极客时间 王争老师的《数据结构与算法之美》课程,通过课后反思以及借鉴各位学友的发言总结,现整理出自己的知识架构,以便日后温故知新,查漏补缺。
初入算法学习,必是步履蹒跚,一路磕磕绊绊跌跌撞撞。看不懂别慌,也别忙着总结,先读五遍文章先,无他,唯手熟尔~
与诸君共勉