哈夫曼树&编码

数据结构 专栏收录该内容
11 篇文章 0 订阅

哈夫曼树&编码

前置芝士:无。

参考资料

https://blog.csdn.net/qq_29519041/article/details/81428934

跳转按钮

讲解构造 \color{#8c4}\texttt{讲解构造} 讲解构造


经典例题 \color{#8af}\texttt{经典例题} 经典例题


讲解构造 \color{#000}\texttt{讲解构造} 讲解构造

什么是最优编码?

平常我们用的字符串,如 kkakioi \texttt{kkakioi} kkakioi,每个字符都是用不同的二进制编码表示的,如 ASCII \texttt{ASCII} ASCII 码。

现在我们考虑编写一个编码方式,使总编码长度最小 kkakioi \texttt{kkakioi} kkakioi 中有四个字母 k \texttt{k} k a \texttt{a} a i \texttt{i} i o \texttt{o} o,分别出现 3 3 3 1 1 1 2 2 2 1 1 1 次。

第一种方法是定长编码。如用长度为 2 2 2 的定长编码 00 00 00 01 01 01 10 10 10 11 11 11 分别表示 k \texttt{k} k a \texttt{a} a i \texttt{i} i o \texttt{o} o,那么编码总长为 14 14 14。貌似这就是最佳方案了。

但是还有一种编码方式——用 1 1 1 01 01 01 001 001 001 000 000 000 来表示 k \texttt{k} k a \texttt{a} a i \texttt{i} i o \texttt{o} o,编码总长竟然只有 13 13 13!这微小的进步有着巨大的意义。

有人会问,如果不定长的编码也可以无歧义识别的话,为什么不用 0 0 0 1 1 1 00 00 00 11 11 11 来代替 k \texttt{k} k a \texttt{a} a i \texttt{i} i o \texttt{o} o 呢?那么看总编码 0011 0011 0011,你怎么知道它是 io \texttt{io} io 还是 kkaa \texttt{kkaa} kkaa 或者还是别的呢?

而上面的编码就没这个问题,它的奥妙就在于:没有任何一个字符的编码是另一个字符的编码的前缀。而这样的编码也有很多个,当中最短的那种编码就是最优编码。正好,上面我们给出的长度为 13 13 13 的编码就是字符串 kkakioi \texttt{kkakioi} kkakioi 的最优编码。


怎么构造最优编码?

用哈夫曼树,这是一种二叉树,每个节点的度要么为 0 0 0(就是表示字母的叶子节点),要么为 2 2 2(就是表示前缀的节点)。可以把哈夫曼树看成一种 trie \texttt{trie} trie 树,边是 0 0 0 1 1 1(向左的边为 0 0 0,向右的边为 1 1 1),然后结束符都在叶子节点上。如下就是上述字符串 kkakioi \texttt{kkakioi} kkakioi 生成的哈夫曼树(节点上的数是频率)。

hfms.jpg
然后每个字符的编码就是根节点到它对应的叶子节点的路径组成的字符串。如 i \texttt{i} i 就是 01 01 01。 因为前缀节点不对应字符,所以生成的编码自然没有一个编码是另一个的前缀。

而生成哈夫曼树的原理只与字符频率有关,具体做法就是先把每个字符当做权值为自己出现次数的节点,然后每次取权值最小的两个节点,构造新节点的权值为他们俩的和,向它们连边,然后之后不考虑这两个节点。

最后总结点数 = = =字符种类 × 2 − 1 \times 2-1 ×21,而构造的实现可以用一个优先队列存放节点。同一个字符串构造的哈夫曼树不唯一。

还有一个叫做哈夫曼树的带权路径长度的东西,就相当于哈夫曼编码下的原字符串总长度。设有 n n n 种字符,第 i i i 种字符的编码长度为 l i l_i li,出现次数为 w i w_i wi,那么带权路径长度 = ∑ i = 1 n l i ⋅ w i =\sum\limits_{i=1}^nl_i\cdot w_i =i=1nliwi

如用代码实现构造哈夫曼树并输出带权路径长度:

code

#include <bits/stdc++.h>
using namespace std;

//&Start
#define lng long long
#define lit long double
const int inf=0x3f3f3f3f;
const lng Inf=1e17;

//&Main
const int N=510;
int n,w[N<<1],cnt,dep[N<<1],ans;
vector<int> g[N<<1];
class cmp{
public:
	bool operator()(int x,int y){//通过比较wi得到小顶堆
		return w[x]>w[y];
	}
};
priority_queue<int,vector<int>,cmp> q;
void dfs(int x){//求li
	for(auto to:g[x]) dep[to]=dep[x]+1,dfs(to);
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&w[++cnt]),q.push(cnt);
	while(q.size()>1){
		int x=q.top(),y;
		q.pop();y=q.top();q.pop();
		w[++cnt]=w[x]+w[y];
		g[cnt].push_back(x);
		g[cnt].push_back(y);
		q.push(cnt);
	}
	dfs(cnt);
	for(int i=1;i<=n;i++)
		ans+=w[i]*dep[i];
	printf("%d\n",ans);
	return 0;
}

经典例题 \color{#000}\texttt{经典例题} 经典例题

合并果子

[NOIp2004]合并果子
给你 n n n 个权值为 w i w_i wi 的果子,每次可以把两个果子合并为一个果子权值为两个果子之和,并耗费两个果子之和的力气。求把所有果子合并成一个最少耗力。
数据范围: 1 ≤ n ≤ 1 0 4 1\le n\le 10^4 1n104 1 ≤ a i ≤ 2 × 1 0 4 1\le a_i\le 2\times 10^4 1ai2×104


每个果子一路合并的过程就像一棵哈夫曼树,最后的带权路径长度就是答案(结论很明显,读者自推)。代码和上面那个一模一样。


荷马史诗

[NOI2015]荷马史诗
给定 n n n 个字符的出现次数 w i w_i wi,求 k k k 进制编码下的最短总编码长度和在达到最短总编码长度的情况下最长编码字符的最短编码长度。
数据范围: 1 ≤ n ≤ 1 0 5 1\le n\le 10^5 1n105 2 ≤ k ≤ 9 2\le k\le 9 2k9


就想当于求 k k k 叉哈夫曼树编码。

同理,每次选权值最小的 k k k 个节点合并。但是因为最终要总编码长度最小,所以浅一点的节点必须满 k k k。因为每次合并减少 k − 1 k-1 k1 个节点,最后剩下 1 1 1 个节点,所以开始时的节点数必须 ≡ 1 ( m o d k − 1 ) \equiv1\pmod{k-1} 1(modk1),插入几个权值为 0 0 0 的节点即可(脑子中模拟一下,这样就可以把权值大的节点顶到浅一点的地方去了)。

至于第二问要求在达到最短总编码长度的情况下最长编码字符的最短编码长度,只需要特判优先队列的排序方式,如果有两个节点权值相同,就把编号小一点的节点排在前面先合并(因为编号小高度就小)。


code

#include <bits/stdc++.h>
using namespace std;

//&Start
#define lng long long
#define lit long double
const int inf=0x3f3f3f3f;
const lng Inf=1e17;

//&Main
const int N=1e5+10;
int n,k,cnt;
lng w[N<<1],dep[N<<1],ans,mx;//不开long long见祖宗
vector<int> g[N<<1];
class cmp{
public:
	bool operator()(int x,int y){
		if(w[x]==w[y]) return x>y;//特判排序
		return w[x]>w[y];
	}
};
priority_queue<int,vector<int>,cmp> q;
void dfs(int x){
	for(auto to:g[x]) dep[to]=dep[x]+1,dfs(to);
}
int main(){
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++)
		scanf("%lld",&w[++cnt]),q.push(cnt);
	while((q.size()-1)%(k-1)) q.push(0);//插0
	while(q.size()>1){
		++cnt;
		for(int i=1;i<=k;i++){//k叉哈夫曼树
			int x=q.top();q.pop();
			g[cnt].push_back(x);
			w[cnt]+=w[x];
		}
		q.push(cnt);
	}
	dfs(cnt);
	for(int i=1;i<=n;i++)
		ans+=w[i]*dep[i],mx=max(mx,dep[i]);
	printf("%lld\n%lld\n",ans,mx);
	return 0;
}

祝大家学习愉快!

  • 2
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2020 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值