引入
合并果子问题如下:
有n堆果子,每次可以合并任意两堆果子,耗费体力值为[两堆果子数之和],最终在n-1次合并后,得到一堆果子。
给出合并的方案,使得耗费的体力值最小
例如有3堆果子,质量依次为1、2、9.那么可以先将质量为1和2的果堆合并,新堆质量为3,耗费体力为3;然后将3与9的果堆合并,得到12,耗费体力为12。所以耗费体力之和为3+12=15。可以证明15为最小的体力耗费值
- 每次合并,都要将被合并的两堆果子数之和作为费用,显然,如果一开始就合并数量大的果堆,这个果堆会在接下来的合并过程中一直对费用有贡献,我们要避免这种情况
- 我们应该尽可能减少数量大的果堆被合并的次数,即将其留到最后再合并
- 因此,策略是:反复选择当前最小的两个元素,合并,直到仅剩一个元素
- 具体实现可以用优先队列完成
这个问题,本质上是一个哈夫曼树的构造问题
哈夫曼树
我们用树来描述上面的合并过程:
假如过果堆为1,2,2,3,6,一种可能的合并过程如下:
计算整个合并过程的总花费:
①可以将所有非叶子节点的值求和
②也可以计算每个叶子节点对总花费的贡献:sum[叶子节点值 * 叶子节点到根节点的距离]
考虑方法②,我们引入几个术语
叶子节点的带权路径长度:叶子节点值 * 叶子节点到根节点的距离
树的带权路径长度WPL(Weighed Path Length of Tree):所有叶子节点的带权路径长度之和
哈夫曼树/最优二叉树:树的带权路径长度WPL最小的树
现在回到上面的合并果子问题
- 原问题转化为:将n个数字作为叶子节点,构造一棵哈夫曼树(即带权路径长度最小的树),返回其最小带权路径长度
- 考虑每个节点对答案的贡献,值越大的叶子节点,应该越接近根部
因此,哈夫曼树的构造思路:反复选择当前最小的两个元素,合并,直到仅剩一个元素
- 最初所有果堆看作叶子节点(视作仅有一个节点的树)
- 合并其中根节点值最小的两棵树,产生一个新的父节点,父节点权值=两个子节点权值之和,这三个节点构成一棵新的树
- 接下来继续从 余下的树的根节点 中,继续选择两个节点合并,重复第2步
- 最终只剩下一棵树,就是哈夫曼树
ps. 对于最小的树的带权路径长度WPL,可能有多种不同的构造方案,即:对于同一组叶子节点,哈夫曼树可以是不唯一的
总结
哈夫曼树的核心思想:
- 考虑每个节点对于最终答案的贡献,其贡献与 [叶子节点值 * 叶子节点到根节点的距离(即带权路径长度)] 有关
- 因此,值越大的叶子节点,应该越接近根部
- 故应该优先合并当前权值最小的两个节点
- 反复选择当前最小的两个元素,合并,直到仅剩一个元素
应用
LeetCode 1130. 叶值的最小代价生成树
同样要求构造二叉树,但是叶子节点从左到右的出现顺序给定,且每次合并两个节点,花费=左子树中的最大值*右子树中的最大值
- 类似哈夫曼树的思想,值越大的叶子,应该越接近根部(这样其对花费的贡献最小)
- 将叶子节点从左到右排列,每次取 [相邻两个元素的最大值] 最小的那两个节点,合并
- 实现过程用数组模拟即可
哈夫曼编码
在传输数据时,可以将不同的字符转化为特定的01编码来传输,例如A为0,B为10,C为11等等,我们要求编码为前缀码,即任何一个字符对应的01串都不能是另一个字符对应的01串的前缀。(否则解码时,不知道某个01串应该翻译为什么字符)
对于一段特定的文本 ,其中的每个字符出现频率不同,我们对其进行前缀码编码,求编码后可能的最短01串长度
分析:
- 如何满足“前缀码”的条件呢?
假想二叉树上某节点,向左儿子的边为0,向右儿子的边为1,那么从根节点到各节点,都能获得其唯一的编码
例如在下图中,C的编码为100
不难发现,任意一个叶子节点,其编码一定不会称为其他节点的编码前缀
图中T节点不是叶子节点,进而T的编码是C的编码的前缀,不符合要求
因此,我们将所有字符都安排在叶子节点即可
- 如何让编码后的文本01串最短呢?
显然,出现频率越高的字符,其编码应该越短,这样最终编码后的01串中,这个字符的01编码对于总长度的贡献能尽量小 - 把字符的出现频率作为其叶子节点的权值,则问题转化为求带权路径长度最小的哈夫曼树问题(字符出现频率/即权值*编码长度/即路径长度=字符对编码后的文本的贡献)
- 策略:所有字符按照出现频率排序,每次取出一个出现频率最低的叶子节点,加入哈夫曼树中,并获得一个新的根节点
这样由哈夫曼树产生的编码就是哈夫曼编码,显然哈夫曼编码就是一种能使特定字符串编码为01串后长度最短的前缀码
ps. 注意,哈夫曼编码是针对特定的字符串而言的,因为其构造需要根据不同字符的出现频率来确定