05-树9 Huffman Codes

199 阅读8分钟

Problem Description

In 1953, David A. Huffman published his paper "A Method for the Construction of Minimum-Redundancy Codes", and hence printed his name in the history of computer science. As a professor who gives the final exam problem on Huffman codes, I am encountering a big problem: the Huffman codes are NOT unique. For example, given a string "aaaxuaxz", we can observe that the frequencies of the characters 'a', 'x', 'u' and 'z' are 4, 2, 1 and 1, respectively. We may either encode the symbols as {'a'=0, 'x'=10, 'u'=110, 'z'=111}, or in another way as {'a'=1, 'x'=01, 'u'=001, 'z'=000}, both compress the string into 14 bits. Another set of code can be given as {'a'=0, 'x'=11, 'u'=100, 'z'=101}, but {'a'=0, 'x'=01, 'u'=011, 'z'=001} is NOT correct since "aaaxuaxz" and "aazuaxax" can both be decoded from the code 00001011001001. The students are submitting all kinds of codes, and I need a computer program to help me determine which ones are correct and which ones are not.

Input Specification

Each input file contains one test case. For each case, the first line gives an integer N (2≤N≤63), then followed by a line that contains all the N distinct characters and their frequencies in the following format:

c[1] f[1] c[2] f[2] ... c[N] f[N]

where c[i] is a character chosen from {'0' - '9', 'a' - 'z', 'A' - 'Z', '_'}, and f[i] is the frequency of c[i] and is an integer no more than 1000. The next line gives a positive integer M (≤1000), then followed by M student submissions. Each student submission consists of N lines, each in the format:

c[i] code[i]

where c[i] is the i-th character and code[i] is an non-empty string of no more than 63 '0's and '1's.

Output Specification

For each test case, print in each line either "Yes" if the student's submission is correct, or "No" if not.

Note: The optimal solution is not necessarily generated by Huffman algorithm. Any prefix code with code length being optimal is considered correct.

Sample Input

7
A 1 B 1 C 1 D 3 E 3 F 6 G 6
4
A 00000
B 00001
C 0001
D 001
E 01
F 10
G 11
A 01010
B 01011
C 0100
D 011
E 10
F 11
G 00
A 000
B 001
C 010
D 011
E 100
F 101
G 110
A 00000
B 00001
C 0001
D 001
E 00
F 10
G 11

Sample Output

Yes
Yes
No
No

Solution

题意理解:给定字符与其频率,判断所给编码是否是最优编码( 注意:哈夫曼编码生成的一定是最优编码,但最优编码不一定是由哈夫曼算法生成的)

哈夫曼编码的特点:

  1. 最优编码 —— 总长度(WPL)最小
  2. 无歧义解码 —— 前缀码:数据仅存于叶结点
  3. 没有度为 1 的结点 —— 满足 1、2 则必然有 3

但满足 2、3 不一定有 1 。本题就用前两个条件判断是否为最优编码。

  • 首先根据所给字符及其频率构造哈夫曼树,求出哈夫曼树的 WPL
  • 其次判断每组编码:
    • 是否为所求的 WPL
    • 是否为前缀码
  • 满足条件即为正确编码,否则错误

完整代码(AC):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct TNode *HuffmanTree;
struct TNode {
    int Data;
    HuffmanTree Left;
    HuffmanTree Right;
};

typedef struct Heap *MinHeap;
struct Heap {
    HuffmanTree *Elements;
    int Size;
    int Capacity;
};

int f[64];

MinHeap Create(int n)
{
    MinHeap H = (MinHeap)malloc(sizeof(struct Heap));
    H->Elements = (HuffmanTree*)malloc(sizeof(HuffmanTree) * (n + 1));
    H->Size = 0;
    H->Capacity = n;
    H->Elements[0] = (HuffmanTree)malloc(sizeof(struct TNode));
    H->Elements[0]->Data = -1;
    H->Elements[0]->Left = H->Elements[0]->Right = NULL;
    return H;
}

void Insert(MinHeap H, HuffmanTree Node)
{
    int i;
    if (H->Size == H->Capacity) return;

    for (i = ++H->Size; H->Elements[i / 2]->Data > Node->Data; i /= 2)
        H->Elements[i] = H->Elements[i / 2];
    
    H->Elements[i] = Node;
}

MinHeap Input(MinHeap H, int n)
{
    char c;
    int i;
    HuffmanTree Node;

    for (i = 0; i < n; i++) {
        scanf("%c %d", &c, &f[i]);
        getchar();
        Node = (HuffmanTree)malloc(sizeof(struct TNode));
        Node->Data = f[i];
        Node->Left = Node->Right = NULL;
        Insert(H, Node);
    }

    return H;
}

HuffmanTree Delete(MinHeap H)
{
    int i, Parent, Child;
    HuffmanTree MinItem, tmp;

    MinItem = H->Elements[1];
    tmp = H->Elements[H->Size--];
    for (Parent = 1; Parent * 2 <= H->Size; Parent = Child) {
        Child = Parent * 2;
        if (Child != H->Size && H->Elements[Child + 1]->Data < H->Elements[Child]->Data)
            Child++;
        if (tmp->Data <= H->Elements[Child]->Data) break;
        else H->Elements[Parent] = H->Elements[Child];
    }
    H->Elements[Parent] = tmp;

    return MinItem;
}

HuffmanTree Huffman(MinHeap H)
{
    int i, times;
    HuffmanTree T;
    times = H->Size;
    for (i = 1; i < times; i++) {
        T = (HuffmanTree)malloc(sizeof(struct TNode));
        T->Left = Delete(H);
        T->Right = Delete(H);
        T->Data = T->Left->Data + T->Right->Data;
        Insert(H, T);
    }
    T = Delete(H);
    return T;
}

int WPL(HuffmanTree T, int Depth)
{
    if (!T->Left && !T->Right)
        return T->Data * Depth;
    
    return WPL(T->Left, Depth + 1) + WPL(T->Right, Depth + 1);
}

void Judge(int n, int CodeLen)
{
    int i, j, Len = 0, flag;
    char c, code[64][64];

    for (i = 0; i < n; i++) {
        scanf("%c %s", &c, &code[i]);
        getchar();
        Len += f[i] * strlen(code[i]);
    }
    if (Len != CodeLen) {
        printf("No\n");
        return;
    }

    HuffmanTree T, p;
    T = (HuffmanTree)malloc(sizeof(struct TNode));
    T->Data = -1;
    T->Left = T->Right = NULL;
    for (i = 0; i < n; i++) {
        p = T;
        for (j = 0; code[i][j]; j++) {
            if (code[i][j] == '0') {
                if (p->Left && p->Left->Data > 0) {
                    printf("No\n");
                    return;
                } else if (!p->Left) {
                    p->Left = (HuffmanTree)malloc(sizeof(struct TNode));
                    p->Left->Data = -1;
                    p->Left->Left = p->Left->Right = NULL;
                }
                p = p->Left;
            } else if (code[i][j] = '1') {
                if (p->Right && p->Right->Data > 0) {
                    printf("No\n");
                    return;
                } else if (!p->Right) {
                    p->Right = (HuffmanTree)malloc(sizeof(struct TNode));
                    p->Right->Data = -1;
                    p->Right->Left = p->Right->Right = NULL;
                }
                p = p->Right;
            }
        }
        if (p->Data > 0 || p->Left || p->Right) {
            printf("No\n");
            return;
        } else
            p->Data = f[i];
    }

    printf("Yes\n");
    return;
}

void FreeHeap(MinHeap H)
{
    free(H->Elements);
    free(H);
}

void FreeTree(HuffmanTree T)
{
    if (T->Left) FreeTree(T->Left);
    if (T->Right) FreeTree(T->Right);
    free(T);
}

int main()
{
    int i, n, m, CodeLen;

    scanf("%d", &n);
    getchar();
    MinHeap H = Create(n);
    H = Input(H, n);
    HuffmanTree T = Huffman(H);
    CodeLen = WPL(T, 0);

    scanf("%d", &m);
    getchar();
    for (i = 0; i < m; i++) Judge(n, CodeLen);

    FreeHeap(H);
    FreeTree(T);
    return 0;
}

代码详解

🐱‍💻 程序框架:

  • 将读入的字符及其频率通过哈夫曼算法构建出哈夫曼树并求得最优带权路径总长 WPL
  • 利用求得的最优 WPL 对每组提交进行判断并输出判断结果
    • 一是要保证最优编码,WPL 最小,等于哈夫曼算法求得的值
    • 二是要保证无歧义,前缀码

✅ 读入字符及其频率并构建最小堆:

  • 将读入的频率存在全局变量 f[i] 中,因为后面判断 WPL 是否最优的时候也要用
  • 首先建立最小堆,然后没读入一个频率就新建一个对应的节点插入堆中

✅ 构建哈夫曼树:

  • 将读入的结点存入一个最小堆中
  • 总共要合并结点个数 - 1 次。注意判断条件不要写 H->Size ,因为在删除插入过程中该值会变化。
    • 合并时先新建一个树结点,左子树从最小堆中取出最小值,右子树从最小堆中取出最小值
    • 再把新建的树结点插入堆中
  • 循环结束后堆中只剩最后一个元素,即为构建好的哈夫曼树,用 Delete 弹出即可
  • 返回构建好的哈夫曼树

❗ Insert 函数(直观想象:插入到数组尾,从数组尾往父结点追溯到待插入元素的位置):

  • 传入参数是最小堆和待插入元素
  • 首先更新堆中元素的数量,初始化 i 为数组最末尾的位置。
  • 当 i / 2 即当前结点的父结点元素值大于待插入元素值,把父结点元素值挪到当前结点位置
    • H->Elements[i] = H->Elements[i / 2];
  • 循环上述操作,直到父结点元素比待插入元素值小或相等,当前 i 指向的结点就是插入位置
  • 退出循环,插入即可
    • H->Elements[i] = Node;

❗ Delete 函数(直观想象:把最小结点取出,把最后一个结点取出,更新 Size ,从顶部向下追溯到原最后一个结点插入的位置):

  • 首先将堆顶元素存到 MinItem 中,最后返回
  • 将数组尾元素存到 tmp 中,更新 Size ,相当于去除数组尾部空间
  • 设置 Parent 为 1 ,即堆顶,Child 很自然就等于 Parent * 2 ,进入循环
    • 如果左子树不为空且不是堆数组最后一个元素,一定存在右子树
    • 此时从左右子树中选出较小的元素更新为 Child ,挑一个小的往上过滤
    • 如果 tmp ,开始被删除的堆数组尾元素,小于等于 Child 元素,跳出循环不用找了已经找到插入位置,即当前 Parent ,break 退出循环
  • 将原堆数组最后一个结点放入当前位置
    • H->Elements[Parent] = tmp;
  • 返回/弹出最小堆堆顶的最小元素。

🤔 求一棵树的 WPL(递归):

  • 传入的参数是哈夫曼树和当前结点相对于原结点的深度,初值为 0
  • 递归结束条件为左右子树都为空,返回结点权重与传入的深度之积
  • 否则返回左右子树的 WPL ,传入左右子树,深度 Depth 在当前基础上 +1

💥 读入提交的编码并判断后输出结果:

  • 传入参数为最优编码的 WPL 值 CodeLen 和 字符数量 n
  • 首先判断是否为最优编码,即 WPL 是否等于之前求得的 CodeLen
    • 将读入的编码存在字符串数组 code[i][j]
    • 每组编码的 WPL = strlen(code[i]) * f[i]
    • 计算完后比对 CodeLen ,不等于直接输出 No 并退出
  • 比对无误后判断是否为无二义性的前缀码,建一棵树来判断
    • 外层循环是遍历每一条编码
    • 内层循环遍历编码的每一位, p 是指向当前结点的指针
      • 如果是 0
        • 查看左孩子,如果左孩子不为空但已经有一个编码位了(体现在 Data 值不等于 -1),编码不正确,输出 No 并退出
        • 如果左孩子为空,新建结点扩展左孩子。p 更新为左孩子
        • 或者不为空但没有编码位(或者说只是一个途径的结点),直接更新 p 为左孩子
      • 如果是 1
        • 查看右孩子,如果右孩子不为空但已经有一个编码位了(体现在 Data 值不等于 -1),编码不正确,输出 No 并退出
        • 如果右孩子为空,新建结点扩展左孩子。p 更新为右孩子
        • 或者不为空但没有编码位(或者说只是一个途径的结点),直接更新 p 为右孩子
    • 遍历完一个字符的编码后
      • 如果停下来的位置已经有编码位了或者左右子树不为空(表示当前是一个途径结点),编码不正确,输出 No 并退出
      • 否则说明这是一个合法的编码位,更新 p 当前的 Data 为 f[i] ,表示这个结点已经是一个编码叶结点了
    • 每个字符的编码都遍历完后建成了一棵编码树表示编码无歧义
  • 最后 WPL 与 前缀码都判断为正确后,输出 Yes 并退出

判断编码正确性的思想是模拟,如果模拟中途发生矛盾说明是错误的,如果模拟成功就说明编码是正确的。