算法思想 - 贪婪(贪心)算法

贪婪算法

什么是贪婪算法

“贪婪”可以理解为,以逐步的局部最优,达到最终的全局最优。即在每一次选择中都选择当前的最优选择,当选择结束的时候,我们就可以获得全局最优解。(注意,在一定条件下,贪婪策略是正确的,但是在很多情况下,它又是错误的。)

分析贪婪的关键步骤

使用贪婪算法有关键的三步,为了理解方便,这里使用一个例子辅助讲解:

假设我们有一个可以容纳 100kg 物品的背包,可以装下各种物品,现在有 5 种豆子,豆子可以自由拆分和搭配,豆子的总量和总价值如下图。请问如何装豆子才能让背包中的物品的总价值最大?
贪心-豆子理解.jpg

你可以先自己思考该怎么解决这个问题,然后我们再看如何用贪婪的思想来解决这个问题:

1.当我们看到这类问题,首先要联想到贪婪算法:对一组数据,我们定义了限制值和期望值,希望从中选出几个数据,在满足限制值的情况下,期望值最大。
在刚才的例子中,限制值->背包承重100kg,期望值->物品总价值最大,数据-> 5 种豆子的价值和总量。

2.尝试使用贪心算法解决问题:根据贪心策略,每次选择在当前情况下,对限制值同等贡献量的情况下,对期望值贡献最大的数据。
在例子中,就是选择单价最高的豆子。

3.验证贪心策略下的结果是否最优。在大部分情况下,只需要举几个例子就可以很方便地验证。

无法使用贪婪的情况

实际上,使用贪心算法解决问题的思路,并不总是能够给出最优解。一下面的例子为例,我们要找一条从 S 到 T 的最短路径:
贪心失效.jpg

使用贪心策略的结果是:S->A->E->T,路径长度为 9 。
而最短路径为:S->B->D->T,路径长度为 6 。

在这个例子中,贪心算法失效的主要原因是,前面的选择会影响之后的选择。你也可以从贪心的思想入手:在很多情况下,局部最优并不一定是全局最优

在生活中我们也经常会遇到这样的情况:一个人在人生的每一个岔路口都做出了当时最好的选择,他在每一次争斗中都胜利了,但是他的一生却并并不如意。我常常将这种事当做悲剧,薛宝钗在一定程度上是这样的人,二战中的日本走的道路也是这样。最终,薛宝钗并未如愿,日本也走向了深渊。(关于这个问题,你可以去听听《冬吴相对论》的第100期:合成谬误的陷阱)

贪心算法的例子

下面给出几个可以使用贪心策略解决的问题的例子:

1.分糖果
我们有 m 个糖果和 n 个孩子。我们现在要把糖果分给这些孩子吃,但是糖果少,孩子多(m 每个糖果的大小不等,这 m 个糖果的大小分别是 s1,s2,s3,……,sm。除此之外,每个孩子对糖果大小的需求也是不一样的,只有糖果的大小大于等于孩子的对糖果大小的需求的时候,孩子才得到满足。假设这 n 个孩子对糖果大小的需求分别是 g1,g2,g3,……,gn。
问题:如何分配糖果,能尽可能满足最多数量的孩子?

分析如下:
限制值:只有 m 个糖果
期望值:满足的孩子最多
数据:孩子的需求 Si 和 糖果的大小 Gi
贪心策略:使用尽量小的糖果满足最容易满足的孩子
具体行为:在未被满足的孩子中,需找对糖果需求最小的,然后给他糖果中能满足他的最小糖果。

2.钱币找零
这个问题在我们的日常生活中更加普遍。假设我们有 1 元、2 元、5 元、10 元、20 元、50 元、100 元这些面额的纸币,它们的张数分别是 c1、c2、c5、c10、c20、c50、c100。我们现在要用这些钱来支付 K 元,最少要用多少张纸币呢?

限制值:找零的面额
期望值:最少的纸币张数
数据:所有纸币的面额
贪心策略:优先使用最大面值
具体行为:你可以自己试试

注意,在现实生活中,RMB的找零可以用贪婪算法解决。但是钱币找零的问题并不能用贪婪解决所有情况。比如我们有三种纸币:100,99,1,现在要 198 元,最优解为 2 个 99 元。而贪婪算法会给出 1 个 100 元 和 98 个 1 元。

3.区间覆盖
假设我们有 n 个区间,区间的起始端点和结束端点分别是[l1, r1],[l2, r2],[l3, r3],……,[ln, rn]。我们从这 n 个区间中选出一部分区间,这部分区间满足两两不相交(端点相交的情况不算相交),最多能选出多少个区间呢?

贪心-区间.jpg

限制值:指定的区间,例如[l1,rn]
期望值:最多的不相交区间
数据:所有区间范围
贪心策略:选择最小范围的不重叠的区间
具体行为:看下图


贪心-区间选择.jpg

经典应用:霍夫曼(哈夫曼)编码

贪婪算法最经典的应用,就是使用霍夫曼编码的方法对数据进行压缩,我们看下面的例子:

假设我有一个包含 1000 个字符的文件,每个字符占 1 个 byte(1byte=8bits),存储这 1000 个字符就一共需要 8000bits,那有没有更加节省空间的存储方式呢?

假设我们通过统计分析发现,这 1000 个字符中只包含 6 种不同字符,假设它们分别是 a、b、c、d、e、f。而 3 个二进制位(bit)就可以表示 8 个不同的字符,所以,为了尽量减少存储空间,每个字符我们用 3 个二进制位来表示。那存储这 1000 个字符只需要 3000bits 就可以了,比原来的存储方式节省了很多空间。不过,还有没有更加节省空间的存储方式呢?

这时候,霍夫曼编码就要登场了,霍夫曼编码不仅会考察文本出现的所有字符集,还会统计文本中字符出现的频率,根据出现的频率,我们给出霍夫曼的编码:
哈夫曼-压缩编码.jpg

上面是霍夫曼编码,由于 a~f 的频率依次递减,所以以人的思维看来,它的编码过程非常简单。但是如何实现一个通用的构造编码的过程呢?这里需要构建哈夫曼树

我们把每个字符看作一个节点,并且辅带着把频率放到优先级队列中。我们从队列中取出频率最小的两个节点 A、B,然后新建一个节点 C,把频率设置为两个节点的频率之和,并把这个新节点 C 作为节点 A、B 的父节点。最后再把 C 节点放入到优先级队列中。重复这个过程,直到队列中没有数据。
哈夫曼-构建哈夫曼树.jpg

现在,我们给每一条边加上画一个权值,指向左子节点的边我们统统标记为 0,指向右子节点的边,我们统统标记为 1,那从根节点到叶节点的路径就是叶节点对应字符的霍夫曼编码。
哈夫曼树-哈夫曼编码.jpg

以上就是贪心算法的全部内容

注:本文章的主要内容来自我对极客时间app的《数据结构与算法之美》专栏的总结,我使用了大量的原文、代码和截图,如果想要了解具体内容,可以前往极客时间

你可能感兴趣的:(算法思想 - 贪婪(贪心)算法)