* 问题描述:
设要传输由 n 种基本字符构成的一段信息。已知每种字符在信息中出现的频次 w[i], 1 ≤ i ≤ n (即第 i 种字符在一段信息中出现 w[i] 次)。现在要对这 n 种字符构造一种不等长无歧义的 k 进制前缀编码,使得传输该段信息的编码总长度最短。求该最短长度。
* 基本思路:
一般算法课都会介绍Huffman编码,它就是该问题在 k = 2 下的一种特例。自然的,会想到用类似Huffman算法来解决该问题。
一种典型的错误:
相信有些同志一开始会直接“无脑”地将Huffman算法中的 2 改成 k,即:首先把 n 个结点依次放入一个小顶堆(频次 w 最小的结点位于堆顶);然后不断地依次从堆顶取出 k 个结点“捏”在一起构成一个新结点,新结点权重为 k 个子结点之和,并将其放回堆中。当堆中的结点数 ≤ k 时,合并成Huffman树的根节点,算法结束。
这种算法有一个致命的问题,即这样做生成的树根结点的孩子可能不满 k 个,这显然不是最短的编码方案。因为把任一非根结点孩子的叶节点改为根结点的孩子后得到的编码长度一定更短。如下图所示。
注意到:(1) 对于满足条件的 k 叉Huffman树,每个结点为根的子树都构成一棵满足条件的 k 叉Huffman树。 (2) 任一子树的根结点的父结点必有 k-1 个兄弟。第(2)点可用反证法证明,如果某子树的根结点的父亲的兄弟不满 k-1 个,则将该子树变为其父结点的兄弟得到的编码总长度一定更短。如下图所示。
这就要求我们,每次捏合结点之后,将其放回堆中后,要么堆里只剩一个结点,即其为整棵树的根;要么能再有 k 个几点再捏合成一个结点。也就是说,若 n > k,除了第一次捏合外,其他必须是刚好每 k 个结点捏合成一个新结点,直至最终捏合成根结点。因为每次捏合就形成某些子树的一个父节点,根据前面第(2)点,它必须刚好有 k-1 个兄弟(除非它是整棵树的根)。那么问题的难点就变成了:如何知道第一次捏合该捏合几个结点呢?如果我们一开始的结点数即字符数 n 刚好能满足每 k 个捏合一个,每 k 个捏合一个,...,最终刚好捏合成根那自然是最好的。如果不能,我们就补充 dn 个频次为 0 的结点,使其满足上述过程。可知:n + dn - k + 1 - k + 1 - ... - k + 1 = 1,每一个“- k + 1”表示拿出 k 个结点合并成一个结点然后放回堆中的故过程。由此可得,n - 1+ dn = m(k - 1),m为某一整数,即 n - 1 + dn 为 k - 1 的整数倍。故要补充的 0 结点个数可以如下计算:
· 若 r = (n - 1) % (k - 1) == 0,则不用补充;
· 否则,额外添加 k - 1 - r 个权重为 0 的结点。
* C++示例代码(该代码只计算最小的编码长度,若要获得编码方案稍加改动即可。):
1 #include <cstdio> 2 #include <queue> 3 #include <functional> 4 using namespace std; 5 6 const int MAXN = 300000; // 最大单词种类数 7 int weights[MAXN]; // 存储各单词的权重 8 long long kHuffman(int n, int k, int * weights); /* 返回k重Huffman编码长度 */ 9 10 int main() 11 { 12 int n, k; // 单词种类数,用k进制串进行编码 13 scanf("%d %d", &n, &k); 14 for ( int i = 0; i < n; ++i ) 15 scanf("%d", weights+i); 16 printf("%lld\n", kHuffman(n, k, weights)); 17 return 0; 18 } 19 20 long long kHuffman(int n, int k, int * weights) 21 { 22 /* 平凡情况 */ 23 if ( n == 0 ) 24 return 0; 25 if ( n == 1 ) 26 return 1; 27 28 /* k重前缀码 */ 29 long long length = 0; 30 priority_queue<long long, vector<long long>, greater<long long> > q; 31 32 for ( int i = 0; i < n; ++i ) 33 q.push(weights[i]); 34 int r = (n - 1) % (k - 1); 35 if ( r ) 36 for ( int i = 0; i < k-1-r; ++i ) 37 q.push(0); 38 39 while ( q.size() > 1 ) 40 { 41 long long v = 0; 42 for ( int i = 0; i < k; ++i ) 43 { 44 v += q.top(); 45 q.pop(); 46 } 47 q.push(v); 48 length += v; 49 } 50 51 return length; 52 }