1. 程式人生 > >演算法作業三-哈夫曼編碼

演算法作業三-哈夫曼編碼

實驗三 哈夫曼編碼

問題描述與實驗目的:

給定n個字母(或字)在文件中出現的頻率序列X=<x1,x2,…,xn>,求出這n個字母的Huffman編碼。為方便起見,以下將頻率用字母出現的次數(或稱權值)w1,w2,…,wn代替。

輸入

輸入檔案中的開始行上有一個整數T,(0<T<=20),表示有T組測試資料。

接下來是T行測試資料的描述,每組測試資料有2行。測試資料的第1行上是一個正整數n,(n<50),表示序列的長度。第2行是n個字母出現的權值序列w1,w2,…,wn,它們均為正整數,相鄰的兩個整數之間用空格隔開。

輸入直到檔案結束。

輸出

對輸入中的每組有n個權值的資料,應輸出n+1行:先在一行上輸出“Case #”,其中“#”是測試資料的組號(從1開始);接下來輸出n行,其第1行到第n行上依次輸出第i個字母出現的次數和相應的Huffman編碼,格式如下:

wi Huffman編碼。

每組測試資料對應的輸出最後結束時加一個空行,以便區分。

為保證Huffman編碼的唯一性,在構造Huffman樹的過程中,我們約定:

1.左兒子標記為0,右兒子標記為1;

2.左兒子的權值>=右兒子的權值;

3.相同權值w的兩個字母x、y,先輸入權值的字母x的Huffman編碼長度不超過後輸入權值的字母y的Huffman編碼長度。

4.合併兩個節點後新的權值應從右到左搜尋、插入到相應的位置。

例如:輸入權值序列8 9 3 4 1 2,其Huffman編碼求解過程如下,參考圖A-J:

注意,如圖C中權值1、2對應的節點合併後得權值為3的新節點,它插入到權值為3的原有節點的右邊。

輸入樣例

2
6
9 8 3 4 1 2
8
60 20 5 5 3 3 3 1

輸出

Case 1
9 00
8 01
3 100
4 11
1 1011
2 1010

Case 2
60 0
20 10
5 1101
5 1110
3 11000
3 11001
3 11110
1 11111

分析:

哈夫曼編碼本質是個貪心的演算法。將每個字元視作一個帶權結點(子樹)。
每次優先選擇權值最小的兩個子樹,將二者合併,權值相加,權值小的作為右子結點(1),權值大的作左子結點(0),形成一個新的子樹。再將這棵子樹放回優先佇列中,重複以上的操作。

本題要輸出每個字元對應的哈夫曼編碼,資料n<50較小,所以不考慮用資料結構建樹,用直接儲存路徑的方法實現。自定義一個結構體,內部記錄子樹的權值和所有葉子結點。
用優先佇列儲存所有子樹,因為堆排序是一種不穩定排序,可能導致相同權值的子樹順序顛倒,所以過載比較運算子,使用兩個關鍵字排序。
兩個子樹合併的時候,將左子樹記錄的葉子結點編碼全部加上''0',右子樹記錄的結點編碼全部加'1'。當佇列中結點數為1時,演算法結束。
最後根據讀入的順序輸出0-1字串即可。

執行結果:

Alt text

OJ測評結果:

Alt text

原始碼:

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 105;

string res[55];

struct node{
    int val,id;
    vector<int> dp;
    bool operator < (const node & rhs) const{       //雙關鍵字排序
        if(val==rhs.val) return id < rhs.id;
        return val > rhs.val;
    }
}vz[maxn];

int main()
{
    #ifndef ONLINE_JUDGE
        freopen("in.txt","r",stdin);
        freopen("out.txt","w",stdout);
    #endif
    int T,cas=1; scanf("%d",&T);
    while(T--){
        int n;
        scanf("%d",&n);
        for(int i=0;i<=n;++i) res[i].clear();

        priority_queue<node> Q;
        for(int i=1,w;i<=n;++i){
            node tmp;
            scanf("%d",&w);
            vz[i].val = w;
            tmp.val = w;
            tmp.id = i;
            tmp.dp.push_back(i);
            Q.push(tmp);             //初始將每個結點視作子樹
        }
        int cnt = n+1;
        while(!Q.empty()){
            node x = Q.top(); Q.pop();
            if(Q.empty()) break;
            node y = Q.top(); Q.pop();      //取出權值最小的兩個子樹
            node t;
            t.val = x.val + y.val;          //合併權值
            t.id = cnt++;
            for(int i=0,sz = x.dp.size() ;i < sz ;++i){          //左子樹
                int id = x.dp[i];
                res[id].push_back('1');
                t.dp.push_back(id);         //合併葉子結點
            }
            for(int i=0,sz = y.dp.size();i < sz; ++i){          //右子樹
                int id = y.dp[i];
                res[id].push_back('0');
                t.dp.push_back(id);
            }
            Q.push(t);
        }
        printf("Case %d\n",cas++);
        for(int i=1;i<=n;++i){
            reverse(res[i].begin(),res[i].end());
            cout<<vz[i].val<<" "<<res[i]<<endl;
        }
        puts("");
    }
    return 0;
}