哈夫曼树和哈夫曼编码
创建时间:2025-07-03
哈夫曼树与如何根据数列构造哈夫曼树
对于一颗有根树,我们称它的带权路径长度为它的所有叶子结点到根的距离乘它的权值的总和,即
其中哈夫曼树是带权路径长度最小的二叉树。
假设给定若干元素并构造哈夫曼树使每个元素都是叶子结点,考虑根据已知元素不断合并得到哈夫曼树(类似合并果子)。我们记非叶子节点 \(i\) 的权值 \(w_i\) 为 \(i\) 的两个儿子的权值之和,那么很容易发现,所有非叶子节点的权值之和就是这棵树的带权路径长度(每个叶子结点的权值都被重复计算了它的祖先个数次,即到根节点距离)。故只要使非叶子节点权值之和最小就能构造出哈夫曼树。假设有元素 \(a\)、\(b\)、\(c\)(\(a\le b\le c\)),则先合并 \(a\)、\(b\),在与 \(c\) 合并的非叶子权值和为 \((a+b)+(a+b+c)=2a+2b+c\);先合并 \(a\)、\(c\),再与 \(b\) 合并的非叶子权值和为 \((a+c)+(a+b+c)=2a+b+2c\);先合并 \(b\)、\(c\),再与 \(a\) 合并的非叶子权值和为 \((b+c)+(a+b+c)=a+2b+2c\)。显然 \(2a+2b+c\le 2a+b+2c\le a+2b+2c\),故最有策略一定是优先合并权值小的元素。所以棵以模仿合并果子,用优先队列存储元素,每次删掉两个元素并放入合并后的元素,直到优先队列的大小为 \(1\)。
那么这么做一定使初始元素是叶子结点吗?答案是肯定的。因为非叶子节点都是通过两个元素合并而来的,即创建非叶子节点时优先队列中一定有其他元素被删除。而初始元素在刚开始便放入优先队列,这个过程只有插入没有删除。故初始元素都是叶子节点。
哈夫曼编码问题的解法
以字符串编码为例,在 ASCLL 码中每个字符都对应一个 \(8\) 位等长二进制编码,但实际上这么做很浪费空间。比如不是每个字符都出现时,我们可以为出现频率最高的 e、a、o 等字母制定更短的代码,如 e 对应 \((0)_2\),a 对应 \((10)_2\),o 对应 \((11)_2\)。由此延伸出了一类问题:给定字符串 \(s\),要求对字符重新编二进制码(不一定为等长编码,但要求每个字符的编码都不是其它字符的编码的前缀),使加密后的字符串长度最短。
首先需要统计每个字符的出现次数,那么加密后长度就是每个字符的出现次数乘它的编码长度的总和。将字符看做二叉树的叶子结点,二叉树上每向左走看做 \(0\),向右走看做 \(1\),走到叶子节点便得到了一串二进制编码,而加密后字符串长度正是这棵树的最权路径长度!故直接根据出现次数建哈夫曼树即可。
代码:
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 1e6 + 50;
string s;
int cnt[MAX_N], ans;
priority_queue<int, vector<int>, greater<int> > q;
int main() {
cin >> s;
for (char ch : s)
cnt[ch]++;
for (int i = 0; i < 128; i++)
if (cnt[i])
q.push(cnt[i]);
if (q.size() == 1)
ans = q.top();
while (q.size() > 1) {
int u = q.top();
q.pop();
int v = q.top();
q.pop();
ans += u + v;
q.push(u + v);
}
cout << ans;
return 0;
}
注:由于当 \(s\) 中只有一种字符时仍要为其编码,所以要加入特判。

浙公网安备 33010602011771号