点击查看代码
/*
8.3 哈夫曼编码
1. 树的带权路径长度
从树的根到任意结点的路径长度与该结点上权值的乘积,称为该结点的带权路径长度。树中所有叶结点的带权路径长度之和称为该树的带权路径长度,记为:
WPL=∑i=1nwili其中,wi 是第 i 个叶结点所带的权值,li 是该叶结点到根结点的路径长度。
2. 哈夫曼树
在含有 n 个带权叶结点的二叉树中,其中带权路径长度最小的二叉树称为哈夫曼树,也称最优二叉树。
3. 哈夫曼算法
哈夫曼算法是哈夫曼树的构建过程,是根据贪心策略得到的算法。主要流程为:
初始化:将所有叶结点看做一棵棵树,那么刚开始我们有一片森林;
贪心:每次选择根节点权值最小的两棵树作为左右子树合并成一棵新的二叉树,这棵新的二叉树节点的权值为左右子树的权值之和;
重复:重复 2 过程,直到森林中所有的树合并成一棵树。
在构建哈夫曼树的合并操作中,就可以计算出带权路径长度:
在合并的过程中,每一棵树的根节点的权值其实等于该树所有叶子结点的权值之和;
在每次合并的时候,由于多出两条路径,此时累加上左右子树的根节点权值,相当于计算了一次叶子结点到这两条路径的长度;
每次合并都把左右子树的权值累加起来,就是最终的带权路径长度。
4. 哈夫曼编码
哈夫曼编码是一种被广泛应用而且非常有效的数据压缩编码,其构造步骤如下:
统计待编码的序列中,每一个字符出现的次数;
将所有的次数当成叶结点,构造哈夫曼树;
规定哈夫曼树的左分支为 0,右分支为 1,那么从根节点走到叶子结点的序列,就是该叶子结点对应字符的编码。
题目: 给你n种不同的字符,每种字符会在字符串里出现a[i]次。
现在要给每个字符分配一个二进制哈夫曼编码(不同字符的编码不能互相前缀,比如 0 不能是 01 的前缀),
要求编码后整个字符串的总长度(每个字符的出现次数 × 其编码长度,再求和)尽可能小,
求这个最短的总长度。
*/
#include <iostream>
#include <queue> // 优先队列(堆)需要的头文件
#include <vector> // 堆的底层容器需要
using namespace std;
typedef long long LL; // 防止数值溢出(n≤2e5,每个数≤1e4,总代价可达2e9+)
const int N = 2e5 + 10; // 适配n≤2e5的规模
LL n; // 数字的个数
// 定义最小堆:priority_queue<类型, 底层容器, 比较规则>
// greater<LL> 表示小顶堆(默认是大顶堆,less<LL>)
priority_queue<LL, vector<LL>, greater<LL>> heap;
int main(){
// 1. 输入处理:读取数字个数和所有数字,加入最小堆
cin >> n;
for(int i = 1; i <= n; i++){
LL x;
cin >> x;
heap.push(x); // 每个数字入堆,堆会自动维护最小元素在顶部
}
// 2. 核心逻辑:贪心合并最小的两个数,累加总代价
LL len = 0; // 记录总代价
// 当堆中元素数量>1时,继续合并(只剩1个时合并完成)
while(heap.size() > 1){
LL x = heap.top(); heap.pop(); // 取出堆中最小的数
LL y = heap.top(); heap.pop(); // 取出堆中第二小的数
LL t = x + y; // 合并这两个数,代价为t
len += t; // 累加总代价
heap.push(t); // 把合并后的数重新入堆,参与后续合并
}
// 3. 输出总代价
cout << len << endl;
return 0;
}