1 概念
1.1 哈夫曼树
- 定义:n个带权叶子结点的二叉树中,带权路径长度最小的二叉树。
- 权:树结点被赋予一个某种一亿的数值;
- 带权路径长度:从树根结点到任意结点的路径长度(经过的边数)与该结点上权值的乘积。
- 性质:
- 1 哈夫曼树可以不唯一,但最小带权路径长度一定是唯一的;
- 2 哈夫曼树只有度为1的结点;
- 3 权越小的结点,离根结点越远。
1.2 哈夫曼编码
- 前缀码:任何一个字符的编码都不是另一个编码的前缀。
- 前缀码存在意义:不产生混淆,让解码能正常进行。
- 哈夫曼编码背景:
- 对于一个给定字符串,有多种前缀码方式,为了信息传递的效率,尽量选择长度最短的编码方式。
- 把每个字符出现的次数(频率)作为各自叶子结点的权值,那么字符编码成01串的长度为这颗树的带权路径长度。
- 而带权路径长度最小的树为哈夫曼树。
- 定义:由哈夫曼树产生的编码方式为哈夫曼编码。
2 实现
2.1 哈夫曼树
- 构造思想:反复选择两个最小的元素,合并,直到只剩一个元素。
- 实现方式:堆或优先队列
2.1.2 示例
合并果子
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
例如有3种果子,数目依次为1,2,9。可以先将1、2堆合并,新堆数目为3,耗费体力为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为12。
所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。
输入
输入包括两行,第一行是一个整数n(1<=n<=10000),表示果子的种类数。
第二行包含n个整数,用空格分隔,第i个整数ai(1<=ai<=20000)是第i种果子的数目。
输出
输出包括一行,这一行只包含一个整数,也就是最小的体力耗费值。输入数据保证这个值小于2^31。
样例输入 Copy
3
1 2 9
样例输出 Copy
15
- 方法一(优先队列):
#include <cstdio>
#include <queue>
using std::vector;
using std::priority_queue;
using std::greater;
//小顶堆优先队列
priority_queue<long long, vector<long long>, greater<long long>> q;
int main(int argc, char const *argv[])
{
int n;
long long temp, x, y ,ans = 0;
scanf("%d", &n);
for (int i = 0; i < n; ++i)
{
scanf("%lld", &temp);
q.push(temp);
}
while(q.size() > 1){
x = q.top();
q.pop();
y = q.top();
q.pop();
q.push(x + y);
ans += x + y;
}
printf("%lld\n", ans);
return 0;
}
- 方法二:小顶堆
#include <cstdio>
#include <algorithm>
using std::swap;
const int MAXN = 20010;
long long heap[MAXN];
int n;
void downAdjust(int low, int high){
int i = low, j = i * 2;
while(j <= high){
if(j + 1 <= high && heap[j + 1] < heap[j]){
j = j + 1;
}
if(heap[j] < heap[i]){
swap(heap[j], heap[i]);
i = j;
j = i * 2;
}else{
break;
}
}
}
void createHeap(){
for (int i = n / 2; i >= 1; ++i)
{
downAdjust(i, n);
}
}
void deleteTop(){
heap[1] = heap[n--];
downAdjust(1, n);
}
void upAdjust(int low, int high){
int i = high, j = i / 2;
while(j >= low){
if(heap[j] > heap[i]){
swap(heap[j], heap[i]);
i = j;
j = i / 2;
}else{
break;
}
}
}
void Insert(int x){
heap[++n] = x;
upAdjust(1, n);
}
int main(int argc, char const *argv[])
{
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
{
scanf("%lld", &heap[i]);
}
createHeap();
long long ans = 0;
long long x, y;
while(n > 1){
x = heap[1];
deleteTop();
y = heap[1];
deleteTop();
Insert(x + y);
ans += x + y;
}
printf("%lld\n", ans);
return 0;
}
2.2 哈夫编码
2.2.1 从叶子到根的逆向顺序构造哈夫曼编码
#include <cstdio>
#include <cstring>
#include <limits.h>
#include <algorithm>
using std::swap;
using std::strcpy;
typedef char *HuffmanCode;
const int MAXN = 110;
typedef struct{
int weight;
int lchild, rchild, parent;
}HuffmanNode, *HuffmanTree;
void selectMin(HuffmanTree HT, int n, int &s1, int &s2){
int min = INT_MAX;
for (int i = 1; i <= n; ++i) {
if(HT[i].parent == 0 && min > HT[i].weight){
min = HT[i].weight;
s1 = i;
}
}
min = INT_MAX;
for (int j = 1; j <= n; ++j) {
if(HT[j].parent == 0 && min > HT[j].weight && j != s1){
min = HT[j].weight;
s2 = j;
}
}
if(s1 > s2){
swap(s1, s2);
}
}
void HuffmanCoding(HuffmanTree &HT, HuffmanCode *&HC,int w[],int n){
// w存放n个字符的权值(均>0),构造哈夫曼树HT,并求出n个字符的哈夫曼树编码HC
if(n <= 1) return;
int m = 2 * n -1;//树的所有结点个数
//0号单元未用
HT = new HuffmanNode[m + 1];
//初始化叶子结点
for (int i = 1; i <= n; ++i) {
HT[i].weight = w[i];
HT[i].lchild = HT[i].rchild = HT[i].parent = 0;
}
//初始化非叶子结点
for (int i = n + 1; i<= m; ++i) {
HT[i].lchild = HT[i].rchild = HT[i].parent = 0;
}
for (int i = n + 1; i <= m; ++i)//建立哈夫曼树
{
int s1, s2;
//在HT[1~i-1]中选择parent为0且weight最小的两个结点,其序号分别为s1,s2
selectMin(HT, i - 1, s1, s2);
HT[s1].parent = HT[s2].parent = i;
HT[i].lchild = s1;
HT[i].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
//从叶结点到根逆向求每个字符的哈夫曼编码
HC = new HuffmanCode[n + 1];
//分配n个字符编码的头指针向量([0]不用)
char* cd = new char[n];//分配编码工作空间
cd[n - 1] = '\0';//编码结束符
for (int i = 1; i <= n; ++i)//逐个字符求哈夫曼编码
{
int star = n - 1;//编码结束位置
for (int c = i, f = HT[i].parent;f != 0; c = f, f = HT[f].parent)
{
//从叶结点到根逆向求编码
if(HT[f].lchild == c){
cd[--star] = '0';
}else{
cd[--star] = '1';
}
}
//为第i个字符串编码分配空间,最后一个空间作为结束符
HC[i] = new char[n - star];
//从cd复制编码(串)到HC
strcpy(HC[i], cd + star);
}
delete []cd;//释放工作空间
}
int main(){
HuffmanTree HT;
HuffmanCode *HC;
int n;
int data[MAXN];
while(scanf("%d", &n) != EOF){
for (int i = 1; i <= n; ++i) {
scanf("%d", &data[i]);
}
HuffmanCoding(HT, HC, data, n);
for (int j = 1; j <= n; ++j) {
printf("%s\n", HC[j]);
}
delete(HT);
delete(HC);
}
return 0;
}
/*
哈夫曼的层序遍历为:100,42,58,23,19,29,29,11,8,14,15,5,3,7,8
Input:
8
5 29 7 8 14 23 3 11
Output:
0110
10
1110
1111
110
00
0111
010
*/
2.2.2 根出发直到叶子构造赫夫曼编码
#include <cstdio>
#include <limits.h>
#include <algorithm>
#include <cstring>
using std::swap;
using std::strcpy;
typedef char *HuffmanCode;
const int MAXN = 110;
typedef struct
{
int parent, lchild, rchild;
int weight;
}HuffmanNode, *HuffmanTree;
void selectMin(HuffmanTree HT, int n, int &s1, int &s2){
int min = INT_MAX;
for (int i = 1; i <= n; ++i) {
if(HT[i].parent == 0 && min > HT[i].weight){
min = HT[i].weight;
s1 = i;
}
}
min = INT_MAX;
for (int i = 1; i <= n; ++i) {
if(HT[i].parent == 0 && min > HT[i].weight && i != s1){
min = HT[i].weight;
s2 = i;
}
}
if(s1 > s2){
swap(s1, s2);
}
}
//无栈非递归遍历哈夫曼树,求哈夫曼编码
void HuffmanCoding(HuffmanTree &HT, HuffmanCode *&HC, int w[], int n){
if(n <= 1) return;
int m = 2 * n - 1;
HT = new HuffmanNode[m + 1];
//分配n个字符编码的头指针向量[0]不用
for (int i = 1; i <= n; ++i) {
HT[i].weight = w[i];
HT[i].parent = HT[i].lchild = HT[i].rchild = 0;
}
for (int i = n + 1; i <= m; ++i) {
HT[i].parent = HT[i].lchild = HT[i].rchild = 0;
}
//构建哈夫曼树
for (int i = n + 1; i <= m; ++i) {
int s1, s2;
selectMin(HT, i - 1, s1, s2);
HT[i].lchild = s1;
HT[i].rchild = s2;
HT[s1].parent = HT[s2].parent = i;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
HC = new HuffmanCode[n + 1];
char* cd = new char[n];
int c = m;
int cdlen = 0;
cd[n - 1] = '\0';
for (int i = 1; i <= m; ++i)
{
HT[i].weight = 0;//遍历哈夫曼树作结点状态标志
}
while(c) {
if (HT[c].weight == 0) {//向左
HT[c].weight = 1;
if (HT[c].lchild != 0) {
c = HT[c].lchild;
cd[cdlen++] = '0';
} else if (HT[c].rchild == 0) {//登记叶子结点的编码
HC[c] = new char[cdlen + 1];
cd[cdlen] = '\0';
strcpy(HC[c], cd);//复制编码(串)
}
} else if (HT[c].weight == 1) {//向右
HT[c].weight = 2;
if (HT[c].rchild != 0) {
c = HT[c].rchild;
cd[cdlen++] = '1';
}
} else {//HT[c].weight == 2, 退回
HT[c].weight = 0;
c = HT[c].parent;
--cdlen;//退回父结点,编码长度减一
}
}
delete []cd;
}
int main(int argc, char const *argv[])
{
HuffmanTree HT;
HuffmanCode *HC;
int n;
int data[MAXN];
while(scanf("%d", &n) != EOF){
for (int i = 1; i <= n; ++i) {
scanf("%d", &data[i]);
}
HuffmanCoding(HT, HC, data, n);
for (int i = 1; i <= n; ++i) {
printf("%s\n", HC[i]);
}
delete(HC);
delete(HT);
}
return 0;
}
/*
Input:
8
5 29 7 8 14 23 3 11
Output:
0110
10
1110
1111
110
00
0111
010
*/