zoukankan      html  css  js  c++  java
  • 哈夫曼编码的一个实际应用

    介绍:

    本问题是来自于课堂上老师关于贪心问题的第三讲.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!='
    '){
                    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的原因个人推测还是老师数据太弱了!

  • 相关阅读:
    MySQL集群常见高可用方案(转)
    upsource使用
    Hystrix 使用
    astah UML 先画图、后编程
    java ThreadLocal 使用
    Java基础 Annotation使用
    LVS+Keepalived+Nginx+Tomcat高可用负载均衡集群配置
    招聘求职学习
    Rotate List 面试题
    vue前台(四点二)
  • 原文地址:https://www.cnblogs.com/Arno-vc/p/14013034.html
Copyright © 2011-2022 走看看