1. 程式人生 > 實用技巧 >哈夫曼編碼的一個實際應用

哈夫曼編碼的一個實際應用

介紹:

本問題是來自於課堂上老師關於貪心問題的第三講.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最小,我們只需要使\(l_k\)最小即可.具體的解決思路便是貪心

    • 貪心:每次我們取出兩個最小的點,用這兩個節點合併成為新的節點,新節點的權重是子節點的權重之和,如此反覆直到只有一個子結點就構建了一個一棵樹,我們稱為哈夫曼樹.

      注:圓內的權重,圓外是對應字元

      可以看到該樹有一下幾個特點:

      • 是一棵二叉樹且沒有度為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' && n<=6){
                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;
}

注:由於寫完已經錯過了交題的時間,程式碼只通過了樣例,恐怕還有些細節性的問題(對於這點我應該很有信心!!!)