哈夫曼编码的一个实际应用

介绍:

本问题是来自于课堂上老师关于贪心问题的第三讲.Huffman编码是最有效的二进制编码,其中贪心策略主要体现在根据频度来设置编码长度.最早在数据结构的便有学习到,当时采用的建树方式是带指针的结构体+小顶堆(使用小顶堆的优势在于堆是动态的,同时也有较高的效率——插入和删除并调整的效率约为O(lgN),查找最小的效率为O(1)),从理论上来说也是比较容易理解的.然而在一般的做题中我们实际只需要用数组模拟即可(好吧,其实也是因为没有学过c++里的堆模板).比较惭愧的是好久没写建树相关的内容了,差点不会写了,因此这里记录一下.

来源

http://139.196.145.92/contest_show.php?cid=1963#problem/C

Description

在课堂上,我们学习了哈夫曼编码的原理和实现方法,上实验课时也动手实现过,后来我们又追加介绍了哈夫曼编码的实际压缩和解压缩的实现方法,并且在课堂上也演示了,但当时我们却忽略了一个环节,那就是实际文件存储时,二进制是比特位,而存储的单位一般是字节,显示时又是按照十六进制的。现在给你一个已经用哈夫曼方法压缩过的十六进制文件,请你解压以便还原成原文。

Input

本问题有多组测试数据,第一行就是测试数据的组数nCase,对于每组测试数据,一共有四个部分,第一部分是一个字典(请注意,字典里可能含有空格!),原文本里面出现的任何字符一定在这个字典里面,并且已经按照使用频度从大到小顺序排列。第二部分是字典里相对应字符的使用频度。第三部分是待解压的行数n。第四部分是n行经过哈夫曼压缩的十六进制数组成的字符串。

Output

输出一共n行,每行就是对应输入的原文(请注意,输出的原文里可能含有空格!)。

Sample Input

1
 AORST
60 22 16 13 6 4
5
7C
F3F2CC3C6FE24D3FC5AB7CC6
98BBD266C6FF81
FE6517F5B6663AF98FE2226676FA80
F317262FCFE662FC99D7D

Sample Output

 AO 
ASAO RST ATOAATS  OSAATRR RRASTO
STROAR  SSRTOAAA      ||Error !
AASS TRAA RRRSSTA RASTAATTTSSSOOAOR       
ASTRO STRAO AASSTRAO SSORAR

分析问题:

  1. 哈夫曼编码介绍

    • 有以下一串字符编码:

      11233324234

      我们需要对其进行二进制(因为哈夫曼编码就是一种二进制编码,依据老师的表述,如果去掉这一限制就很难称得上说有最好的编码了)压缩以使最后获得编码最小.

    • 显然,基本的编码策略是针对不同的字符进行不同的编码压缩,让我们列出它们的种类和频度(这一字符在语句出现的次数)来进行比较一下,对于这个集合我们可以称为字典(包含了所有的字符):

      序号 字符 频度
      1 1 2
      2 2 3
      3 3 4
      4 4 2
    • 首先,我们需要建立对应的数学模型.设总的编码长度为wpl,每个字符的编码长度为\(l_k\),每个字符的频度(也就是权值)为\(w_k\),则\(wpl=l_k*w_k\)(0<=k<=字符的个数),其中\(w_k\)是确定的,为了使wpl最小,当\(w_k\)比较大的时候,我们需要使\(l_k\)尽量小.具体的解决思路便是贪心

    • 贪心:每次我们取出两个最小的点,用这两个节点合并成为新的节点,新节点的权重是子节点的权重之和,如此反复直到只有一个子结点就构建了一个一棵树,我们称为哈夫曼树.

      image-20201120201302743

      注:圆内的权重,圆外是对应字符

      可以看到该树有一下几个特点:

      • 是一棵二叉树且没有度为1的节点
      • n个字符节点均为叶节点.由于我们每次总是去掉2个结点,增加一个节点直到最后生成的一个节点,所以最后会产生n-1个结点,共2n-1个结点
    • 依据要求,关于编码我们可以确立两个基本的准则:

      • 这个编码应该越简单越好

      • 为了便于解析以及不引起混淆,一个编码不应该是另一个编码的前缀和

        如对于:0,01这两个编码显然是无效的.

    • 基于霍夫曼树,我们只要对树的左右边进行标号即可——左0右1或者左1右0,由此我们可以得出霍夫曼编码的另一个特定是不唯一.最终的编码(以左0右1为例):

      序号 符号 Huffman编码
      1 1 000
      2 2 01
      3 3 1
      4 4 001

    注:有时对于同一个字典可以构造不同形式的Huffman(如频度相同的字典),也就是异构的.

  2. Huffman构造

    • 数据结构:结构体数组,因为这里我们在意的仅仅是节点之间的父子关系,使用对应的属性记录即可.
    • 求取最小值与倒数第二小的值:由于整道题的数据范围并不大,每一轮我们可以遍历一次,先判断当前数是不是小于最小值,如果小于,先将最小值赋值给倒数第二小的值,再将当前遍历到的值赋给最小值,否则将当前遍历到的值赋给倒数第二小的值.
  3. 解码

    • 将原数据的十六进制编码转化为二进制编码:这里看到老师巧妙地使用了这个数组:

      string  hToBin[20] = {"0000","0001","0010","0011","0100","0101","0110","0111","1000","1001","1010","1011","1100","1101","1110","1111"};
      
    • 转码:使用一个map,另外还有一个无法判断非法编码的问题,我们可以通过编码的最大长度进行判定.

  4. 扩展

    • 有损压缩与无损压缩

      无损压缩:如Huffman编码

      有损压缩:如常见的mp4,mp3,牺牲了一些人耳不太敏感的频段.

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <map>
#include <string>
#define INF 0x3f3f3f3f

using namespace std;
typedef long long ll;

const int N=1e3+10;

typedef struct{
    int parent;
    int left;
    int right;
    int weight;
    char c;
    bool used;
}Node;

Node arr[2*N];

int maxLen;

//将code向原文映射
map<string,string>  codeMap;

void getCode(int index,string str);
string hexToBin(string str);
int max(int a,int b);

int main()
{
    int n,t;
    while(scanf("%d",&t)!=EOF){
        while(t--){
            //节点初始化
            for(int i=0;i<2*N;i++){
                arr[i].parent = 0;
                arr[i].left = 0;
                arr[i].right = 0;
                arr[i].weight = 0;
                arr[i].used = false;
                arr[i].c = -1;
            }
            codeMap.clear();
            char temp;
            scanf("%*c%c",&temp);
            n=0;  //忘记初始化的垃圾!!!
            while(temp!='\n'){
                arr[++n].c = temp;
                // cout << "temp:" << temp << endl;
                scanf("%c",&temp);
            }
            for(int i=1;i<=n;i++){
                scanf("%d",&arr[i].weight);
                // cout << "i:" << i << endl;
            }
            //做结构化的消解,(2*n-1)-(n)
            arr[0].weight = INF;
            int num=n;  //记录所有节点个数
            for(int i=1;i<=n-1;i++){
                int minn=0,minn2=0;
                for(int j=1;j<=num;j++){
                    if(arr[j].used==false&&arr[j].weight<arr[minn].weight){
                        //先将minn转换为minn2
                        minn2 = minn;
                        minn = j;
                    }else if(arr[j].used==false&&arr[j].weight<arr[minn2].weight){
                        minn2 = j;
                    }
                }
                if(minn == 0){
                    break;
                }
                //设置父节点
                num++;
                arr[num].weight = arr[minn].weight+arr[minn2].weight;
                arr[num].left = minn;
                arr[num].right = minn2;
                //设置根节点
                arr[minn].parent = num;
                arr[minn].used = true;
                arr[minn2].parent = num;
                arr[minn2].used = true;
            }
            //进行编码
            maxLen = 0;  //最长编码长度
            getCode(num,"");
            //解析
            int m;
            scanf("%d",&m);
            //cout << m << endl;
            while(m--){
                string str;
                cin >> str;
                str = hexToBin(str);
                string temp = "";
                int len = str.length();
                for(int i=0;i < len;i++){
                    temp += str[i];
                    if(codeMap.count(temp) == 1){
                        cout << codeMap[temp];
                        temp = "";
                    }else if((int)temp.length() > maxLen){
                        // cout << "temp:" << temp << endl;
                        cout << "||Error !";
                        break;
                    }
                }
                if(temp != ""){
                    cout << "||Error !";
                }
                cout << endl;
            }
        }
    }
    return 0;
}

void getCode(int index,string str){
    //递归出口,生成哈夫曼编码并且放在Map中
    // cout << "index:" << index << " arr[index].c:" << arr[index].c << endl;
    if(arr[index].c != -1){
        codeMap[str] = arr[index].c;
        //cout << "str:" << str << " index:" << index << endl;
        maxLen = max(maxLen,str.length());
        return ;
    }
    getCode(arr[index].left,str+'0');
    getCode(arr[index].right,str+'1');
}

int max(int a,int b){
    return a>b?a:b;
}

string hexToBin(string str){
    //直接做映射,老师的这个方法确实不错
    string  hToBin[20] = {"0000","0001","0010","0011","0100","0101","0110","0111","1000","1001","1010","1011","1100","1101","1110","1111"};
    string ans = "";
    int len = str.length(),temp;
    for(int i=0;i<len;i++){
        if(str[i]>='A'){
            temp = str[i]-'A'+10;
        }else{
            temp = str[i]-'0';
        }
        ans += hToBin[temp];
    }
    return ans;
}

注:由于写完已经错过了交题的时间,代码只通过了样例,恐怕还有些细节性的问题(对于这点我应该很有信心!!!)

注(11.25):代码已更新,因为没有去掉调试的两个内容导致错了好几发,最后还是提交成功了,但是之所以能ac的原因个人推测还是老师数据太弱了!

posted @ 2020-11-20 21:05  Arno_vc  阅读(1413)  评论(0编辑  收藏  举报