文章出处:极客时间《数据结构和算法之美》-作者:王争。该系列文章是本人的学习笔记。
1 背豆子的例子
假设我们有一个可以容纳 100kg 物品的背包,可以装各种物品。我们有以下 5 种豆子,每种豆子的总量和总价值都各不相同。为了让背包中所装物品的总价值最大,我们如何选择在背包中装哪些豆子?每种豆子又该装多少呢?
我们最直接的思路是算出每种豆子每kg的价值,按照价值从大到小排序。先装价值高的。单价从高到低排列,依次是:黑豆、绿豆、红豆、青豆、黄豆,所以,我们可以往背包里装 20kg 黑豆、30kg 绿豆、50kg 红豆。
2 贪心解决问题的步骤
1 当我们看到这类问题的时候想到要用贪心:针对一组数据,定义限制值和期望值,希望从中选择几个数据,在满足限制值的情况下,期望值最大。
例子中限制值是背包总容量,期望值是总价值最大。
2 我们尝试看下这个问题是否可以用贪心解决:每次选择当前情况下,在对限制值同等贡献量的情况下,对期望值贡献最大。
优先选择黑豆,在同样装1kg的时候,增加总价值最多。
3 我们举几个例子看下,用贪心得到的结果是不是最优的。大部分情况下,举几个例子就可以,贪心的证明是非常复杂的。
3 贪心实战例子
3.1 分糖果
有m个糖果,n个孩子。我们要把这个糖果分给孩子,一个孩子最多只能有一个糖果。糖果数量小于孩子数量。
每个糖果的大小也不同,这m个糖果的大小分别为s1、s2…sm。每个孩子对糖果的需求量不同,分别为t1、t2…tn。只有糖果大小大于等于孩子需求量的时候,孩子才能满足。
怎么分配才能让满足更多的孩子。
限制值是:糖果的数量。期望值是:获得满足的孩子的个数越多越好。
我们尝试用贪心解决。先满足需求量低的孩子。因为需求量低的孩子更容易满足,而且无论孩子需求量大小,满足一个,就可以让期望值加1。糖果按照从小到大排序,在满足当前孩子需求量的范围内选择最小的糖果。更大的糖果应该用来满足需求量更大的孩子。糖果无论大小,分配一个,就是给限制值加1。
所以,分配方案是:孩子按照需求量从小到大排序,糖果选择满足需求量的最小的糖果。可以满足更多的孩子。
3.2 钱币找零
当给别人找零的时候,我们希望找零的钱的数量越少越好。当然,钱的总量是要足够的。例如找25元,一张20+一张5元是最好的选择。
3.3 区间覆盖
假设我们有 n 个区间,区间的起始端点和结束端点分别是 [l1, r1],[l2, r2],[l3, r3],……,[ln, rn]。我们从这 n 个区间中选出一部分区间,这部分区间满足两两不相交(端点相交的情况不算相交),最多能选出多少个区间呢?
我们假设最左端是lmin,最右端是rmax。这就要求在[lmin,rmax]区间上找到数量更多的不重合的线段。我们按照左边端点从小到大排序这几个区间。每次选择的时候左边端点和前面已经覆盖的区间不重合,右边端点又尽可能的小。这样可以让剩下的未覆盖的区间更大,未来放置更多的区间段。
4 霍夫曼压缩编码
假设一篇文章有1000个字符,每个字符1个字节=8位。总共需要8000个bits。
如果这1000个字符只包含6种字符,分别为a、b、c、d、e、f。只需要3位就可以表示这6个字符。总共需要3000个bits。比最开始空间减少了。
a(000)、b(001)、c(010)、d(011)、e(100)、f(101)
是否可以再压缩呢?使用霍夫曼编码。
霍夫曼编码不仅考察文件中有多少种不同的字符,还统计不同字符的出现频率。根据频率不同,选择不同长度的编码。根据贪心思想,字符频率越高,使用的编码稍短;字符频率低,使用的编码稍长。当编码不等长的时候,怎么从压缩文件中读字符呢?这个时候我们要求一个字符不能是另外一个字符的前缀。
假设这 6 个字符出现的频率从高到低依次是 a、b、c、d、e、f。我们把它们编码下面这个样子,任何一个字符的编码都不是另一个的前缀,在解压缩的时候,我们每次会读取尽可能长的可解压的二进制串,所以在解压缩的时候也不会歧义。经过这种编码压缩之后,这 1000 个字符只需要 2100bits 就可以了。
4.1 设计霍夫曼树
4.1.1 插入队列,形成一棵树
如何设计字符编码呢?按照出现频率由低到高排序。我们把每个字符看作一个节点,并且付带着把频率放到优先级队列中。我们从队列中取出频率最小的两个节点 A、B,然后新建一个节点 C,把频率设置为两个节点的频率之和,并把这个新节点 C 作为节点 A、B 的父节点。最后再把 C 节点放入到优先级队列中。重复这个过程,直到队列中没有数据。
4.2 编码
现在,我们给每一条边加上画一个权值,指向左子节点的边我们统统标记为 0,指向右子节点的边,我们统统标记为 1,那从根节点到叶节点的路径就是叶节点对应字符的霍夫曼编码。