霍夫曼编码的最优性

前缀编码

前缀编码,即没有一个编码是另一个编码的前缀,这样的编码成为前缀编码。

​ 那么前缀编码有什么好处呢?

​ 举个(栗子)吧 如果你有如下的编码

a b c d
编码 1 0 10 01

​ 那么如果你拿到一个编码后 你想要吧\(ca\)编码,也就是\(101\),但是又如何把\(101\)那么你该如何翻译出原串呢?

​ 答案有很多种\(aba\),\(ca\),\(ad\)这些都是一些可能解,那么也就是说这种编码无法让原码与编码一一对应,也就是说你无法通过编码准确地了解原码,这显然是不行的 问题在哪里呢?就在于在编码过程中\(a\)\(c\)的前缀,\(b\)\(d\)的前缀,这使得我们无法清晰地找到唯一解

​ 而前缀编码就很好地解决了这一点,既然没有一个编码是另一个编码的前缀,那么它表达的结果就只有一个,而对应的解码过程也十分简单,有一种方法就是制定一颗给定编码的二叉树,每次往下走,如果走到了对应的叶子结点(也就是对应一个字符)就将其加入,然后再从根节点往下走,反之重新从根节点开始。

​ 举个例子 有以下编码

a b c d
编码 1 01 001 000

那么对于\(101001000\),就只有\(abcd\)一种可能了。

信息熵

如果我们要传递一组数据 比如抛硬币(总共有\(2\)中可能)扑克的花色(\(4\)种可能) 幸运大转盘(\(8\)种可能),那么对应需要多少比特呢?

对应的答案是一位,两位和三位

熵1

很显然若要传递\(k\)种数据,需要\(log_2{k}\)位的比特,在信息论开山之作《通信的数学原理》中,香农就提出了“信息量”的概念。

我们发现以上都是等概率的事件,就比如幸运大转盘每种颜色的出现的概率都是相同的。那么如果是非等概率事件呢?相比于理想的等可能事件构成的系统,我们遇到的更多情况是事件发生的可能性不一样的系统,比如我构造一枚奇怪的硬币,使得它正面朝上的概率是\(0.8\),反面超上的概率是\(0.2\),事件不再是等可能。

​ 但是我们可以把它转换为等可能盖事件出现的概率,比如\(0.2\),我们可以想象成在\(5\)个球中等概率抽中一个球的概率,而\(0.8\)则可以想象成在\(1.25\)个球中抽中等概率选定某个特定球的概率。所以说,对于一个概率为\(p\)的事件,我们只需要用\(\large\frac{1}{p}\)就可以求出我们所想象出来的等可能事件系统中事件的数量。

面对等概率系统,我们可以算出它的“信息量”,再乘以它出现的概率,就是最后的信息量,比如上文的抛硬币

\(0.2*log_20.2+0.8*log_20.8=0.72\)

也就是说至于要\(0.72\)个比特就可以将其传输出去

那么对于一般情况下(\(p_i\))可以如下表示\(H=\sum_{1}^{n}p_i*log_2\frac{1}{p_i}\)

也就\(H=-\sum_1^{n}p_i*log_2p_i\)

香农参考了玻尔兹曼定理之后将其定义为信息熵,并发表在伟大的《通信的数学原理》(有兴趣的同学可以在下文的参考文献中看看原文)

那么这有什么用呢?

正如我们无法突破光速一般,信息熵就是传输理论上的极限,任何想要小于信息熵的编码都是不可能的,也就是徒增烦恼。

也就是说,一个\(m\)个字符组成的文章,有\(n\)种字符将其编码,每种出现的概率是\(p_i\)理论上最优的编码需要

\(Ans=(-\sum_1^{n}p_i*log_2p_i)*m \ \ \ \ bit\)这是理论上的最优解。

一种编码的代价

​ 香农(还是他)还提出了编码的代价,考虑上文信息熵的概念,编码一个文件\(C\)所需的位数

\(Ans(T)=\sum_{c \in C}f(c)*depth(c)\)

​ 其中,二叉树(只能用0/1编码)\(T\),对于每一个字符\(c\),\(f(c)\)为其频率,\(depth(c)\)为其在树中的深度,也就是编码的长度

​ 想要对其有进一步学习的同学可以参考《通信的数学原理》(见参考文献)

贪心算法的正确性

​ 下面我们引入贪心算法。何为贪心算法?顾名思义就是只考虑当前的最优解。

​ 比如经典的找零游戏,有无限多枚\(0.5\)元,\(0.1\)元,\(0.01\)元的硬币,你需要把\(a\)元找零并且使得找零次数最少,那么很显然就是先全部找\(0.5\)元的零钱,再找\(0.1\)元的,最后找\(0.01\)元的。

​ 换句话说,贪心算法会把求解的问题分成若干个子问题,对每个子问题求解,得到子问题的局部最优解,并最后得到把子问题的局部最优解合成原来问题的一个解。但是贪心算法不一定是正确的,因为贪心算法没有考虑到所有的数据,当前结果都是基于它前一步的数据而计算出的局部最优结论,缺乏瞻前顾后和统筹全局的能力。下面介绍贪心算法的正确条件,即若满足以下条件,该贪心算法就是正确的。

​ 对于贪心算法的成立条件总共有两个:1.贪心选择属性: 一个全局最优解可以通过局部最优(贪心)来得到,也就是说我们可以只考虑当前的状况而不考虑其子问题的影响。2.最优子结构: 如果它的一个最优解包含了其他子问题的最优解,那么问题就有最优子结构。

​ 至于为什么同时满足贪心选择属性和最优子结构,贪心算法就是正确的,有兴趣的同学可以自行参考《算法导论》第\(16\)章。

霍夫曼编码

\(1952\)年由霍夫曼提出了霍夫曼编码,大致思路如下

先按出现的概率大小排队,把两个最小的概率相加,作为新的概率和剩余的概率重新排队,再把最小的两个概率相加,再重新排队,直到最后只剩一个元素。每次相加时都将“0”和“1”赋与相加的两个概率,读出时由该符号开始一直走到最后的“1”, 将路线上所遇到的“0”和“1”按最低位到最高位的顺序排好,就是该符号的霍夫曼编码。 ---------摘自《百度百科》

例如我们需要编码\(do\ or \ not \ do\)一般情况下每个字符需要\(8\)位,那么需要\(96\)位存储空间

我们分析一下\(o\)出现\(3\)次,$\ $(空格)出现两次 那么我们按照频率建立如下霍夫曼树

字符 编码
o 00
空格 01
d 100
r 101
n 110
t 111

霍夫曼编码2

如此存储\(do \ or \ not \ do\)只需要\(1000001001010110000011100011\)这样\(29bit\)远小于\(96bit\),同时也可以保证它编码的唯一性

\(up\)主统计了用霍夫曼编码编码《伟大前程》全书后,需要\(4503347\)比特,而字符总量是\(993708\),平均每个字符\(4.53\)比特,这已经非常接近信息熵给出的极限了\(4.49\)比特,那么在下文中会介绍霍夫曼最优编码的证明。

霍夫曼编码最优解的证明(证明前请了解编码的代价和贪心算法正确性的前提)

​ 既然霍夫曼编码是一种贪心算法,就要证明其贪心选择属性和最优子结构。下面分别给出证明:

​ 我们先证明一个贪心选择属性:

​ 在文章\(C\)中,每个字符\(c \in C\)有频率\(f[c]\),设\(x,y\)为最低频率的两个字符,则存在一种最优编码使得\(x,y\)编码长度相同但最后一位不同。

​ 证明:假设,当前的编码是最优解,代价为$ Ans\(,而\)x,y\(为最低的频率的点,不妨设\)f[x]<=f[y]\(,且不在具有最大深度的兄弟叶节点上,设两个具有最大深度的兄弟叶节点字符\)a,b\(,且不妨设\)f[a]<=f[b]$ ,由于\(x,y\)频率最低,故有\(f[x],f[y]<=f[a],f[b]\),设将\(a\)\(x\)调换之后权重为\(Ans2\)

\(Ans-Ans2=f[x]*depth_x+f[a]*depth_a-f[x]*depth_a-f[a]*depth_x\)

\(=(f[a]-f[x])*(depth[a]-depth[x])>=0\)

同理可以证明如果将\(b\)\(y\)调换,\(Ans\)会变小 于是\(Ans>=Ans1,Ans>=Ans2\),又\(Ans\)是最优解,于是\(Ans=Ans1=Ans2\)也就是说替换之后仍然是最优解,于是证明了该引理。

​ 下面证明霍夫曼编码满足最优子结构:

​ 对于一个字符表,构造前缀编码\(T\),移去其频率最低的两个字符\(x,y\)的节点,加上频率为\(f[x]+f[y]\)的节点\(z\)后存在一个最优前缀编码\(T'\),我们先证明将\(T'\)中频率为\(f[x]+f[y]\)的节点\(z\)拆成\(x,y\)得到的前缀编码\(T\)也是最优的。

​ 证明:设原来的树为\(T\),移去\(x,y\)后树为\(T'\),对于每一个不是\(x,y\)的字符,它们的代价是一样的,而\(depth_x=depth_y=depth_z+1\)

\(f[x]*depth[x]+f[y]*depth[y]=(f[x]+f[y])*(depth[z]+1)=f[z]*depth[z]+f[x]+f[y]\)

​ 也就是说\(Ans[T]=Ans[T']+f[x]+f[y]\)

​ 使用反证法,如果存在另一棵树\(T''\)\(T\)更优即\(Ans[T'']<Ans[T]\) 由引理知,\(T''\)\(x,y\)兄弟节点,将\(x,y\)合并成\(T'''\)

\(Ans[T''']=Ans[T'']-f[x]-f[y]<Ans[T]-f[x]-f[y]=Ans(T')\)

​ 但是又\(T'\)是最优前缀编码,那么与前面的假设矛盾,所以\(T\)必定是原字母表中的最优前缀编码

​ 如此证明了霍夫曼编码的最优子结构和贪心选择属性,也就证明了霍夫曼编码作为一种贪心算法的正确性。

这个证明有什么用

​ 很显然,证明这个之后,可以得出霍夫曼编码与香农提出的信息熵基本是一致的,而且因为在大数据统计下,平均每种字符的出现频率与概率都是一样的,也就是大约\(4.49\)比特(大约在直接编码的\(20\% - 90 \%\)左右),那么一篇很长的文章用霍夫曼编码后就可以直接求出它的期望的需要的比特,字节数了。

​ 同时,因为霍夫曼编码是最优的,所以它已经广泛地运用到了数据压缩的计算机程序中,例如调制解调器,网络通讯,高质量电视广播等等领域,成为了人们日常生活中不可缺少的一部分。

代码(C++语言编写)

代码实现:用小跟堆使得每次都找到两个最小的概率,合并后再放回小跟堆中

与此同时用\(string\)数组统计答案即可

#include <cstdio>
#include <algorithm>
#include <queue>
#include <iostream>
#include <cstring>
using namespace std;
const int N=10005;
struct node {
	int x;
	vector<int> v;
	bool operator < (const node &t)const{ //重载运算符
		return x>t.x;
	} 
};
priority_queue<node> Q;
vector<int> V;
char st[N];
int n;
string ans[N];
int main(){
	scanf("%d\n",&n);
	//请输入n个字符 以及它出现的概率(保留三位小数) 
	for (int i=1;i<=n;++i){
		V.clear();V.push_back(i);
		st[i]=getchar();
		double k;
		cin >> k;getchar();k=k*1000;
		Q.push((node){(int)k,V});
	}
	cout << "霍夫曼编码是" << endl; 
	while (Q.size()>1){
		node now1=Q.top();Q.pop();
		node now2=Q.top();Q.pop();
		for (int i=0;i<now1.v.size();++i) ans[now1.v[i]]='0'+ans[now1.v[i]];
		for (int i=0;i<now2.v.size();++i) ans[now2.v[i]]='1'+ans[now2.v[i]];
		V.clear();
		for (int i=0;i<now1.v.size();++i) V.push_back(now1.v[i]);
		for (int i=0;i<now2.v.size();++i) V.push_back(now2.v[i]);
		Q.push((node){now1.x+now2.x,V});
	}
	for (int i=1;i<=n;++i) cout << st[i] << ": " << ans[i] << endl;
	return 0;
}
/*
5
a 0.2
b 0.3
c 0.22
d 0.23
e 0.05
*/

参考文献

1.《算法导论 》原书第二版 \(Introduction \ to \ Algorithms \ Second \ Edition\)

2.如何理解信息熵

3.《通信的数学原理》 香农

4.霍夫曼编码 百度百科

5.什么是霍夫曼编码

特别鸣谢印张悦学长的提示,全文手打,可能会有较多错别字

posted @ 2022-10-11 15:57  zhou_yk  阅读(507)  评论(0编辑  收藏  举报