1. 简介
哈夫曼编码是一种用于无损数据压缩的前缀编码方法,通过构建一棵二叉树来分配可变长度的编码给不同的字符,出现频率较高的字符会被赋予较短的编码,低频字符则会被赋予较长的编码。
2. 原理
该算法基于贪心策略,其核心在于优先处理权重较小的结点并逐步合并成更大的结点直至形成完整的树形图。具体来说,在每次迭代过程中选取两个具有最小权值(即频率最低)的数据组合成一个新结点,并将其加入待选集合继续参与后续操作直至只剩下一个根结点为止。
3. 树的带权路径长度
从树的根到任意结点的路径长度与该结点上权值的乘积,称为该结点的带权路径长度。树中所有叶子结点的带权路径长度之和称为该树的带权路径长度,记为:
其中, 是第 i 个叶子结点所带的权值,
是该叶子结点到根节点的路径长度。
4. 哈夫曼树
在含有 n 个带权叶结点的二叉树中,其中带权路径长度最小的二叉树成为哈夫曼树,也称最优二叉树。
5. 哈夫曼算法
哈夫曼算法是哈夫曼树的构建过程,是根据有贪心策略得到的算法,主要流程为:
- 初始化:将所有叶子结点看做一棵棵树,那么刚开始我们有一片森林。
- 贪心:每次选择根结点权值最小的两棵树作为左右子树合并成一棵新的二叉树,这棵新的二叉树根结点的权值为左右子树的权值之和;
- 重复 2 过程,直到森林中所有的树合并成一棵树。
在构建哈夫曼树的合并操作中,就可以计算出带权路径长度:
- 在合并的过程中,每一棵树的根结点的权值其实等于该树所有叶子结点的权值之和;
- 在每次合并的时候,由于多出来两条路径,此时累加上左右子树的根结点权值,相当于计算了一次叶子结点到这两条路径的长度;
- 每次合并都把左右子树的权值累加起来,就是最终的带权路径长度。
结论:树的带权路径长度又等于除根结点之外其他所有结点的权值加和。
6. 哈夫曼编码
哈夫曼编码是一种被广泛应用而且非常有效的数据压缩编码,其构造步骤如下:
- 统计待编码的序列中,每一个字符出现的次数;
- 将所有的次数当成叶结点,构成哈夫曼树;
- 规定哈夫曼树的左分支为 0 ,右分支为 1 ,那么从根结点走到叶子结点的序列,就是该叶子结点对应字符的编码。
7. 模板题
题目来源:牛客网
题目链接:哈夫曼编码
题目描述:
给出一个有 n 种字符组成的字符串,其中第 i 种字符出现的次数为 。请你对该字符串应用哈夫曼编码,使得该字符串的长度尽可能短,求编码后的字符串的最短长度。
输入描述:
第一行输入一个整数 n (1 ≤ n ≤ 2 × ) ,表示字符种数。
第二行输入 n 个整数 (1 ≤
≤
) ,表示每种字符的出现次数。
输出描述:
输出一行一个整数,表示编码后字符串的最短长度。
示例一:
输入:
3
123
输出:
9
解法:字符串的最短长度 = 字符出现的次数 * 该字符的编码长度,字符出现的次数 即:叶子结点的权值,字符编码的长度 = 根节点到叶子结点的路径长度;
题目就是在问树的带权路径长度的最小值,也就是要构建哈夫曼树。
利用结论:树的带权路径长度又等于除根结点之外其他所有结点的权值加和,我们来进行计算:
参考代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int n;
priority_queue<LL, vector<LL>, greater<LL>> heap; // 小根堆
int main(){
cin >> n;
for(int i = 1; i <= n; i++){
LL x; cin >> x;
heap.push(x);
}
LL ret = 0;
while(heap.size() > 1){ // 构建哈夫曼树
LL x = heap.top(); heap.pop();
LL y = heap.top(); heap.pop();
ret += x + y; // 利用结论
heap.push(x + y);
}
cout << ret << endl;
}