PTA 哈夫曼編碼 (30 分)

PTA 哈夫曼編碼 (30 分)

有一段時間沒寫博客了,不能停止更新,發幾個數據結構練習題的題解

哈夫曼編碼

給定一段文字,如果我們統計出字母出現的頻率,是可以根據哈夫曼算法給出一套編碼,使得用此編碼壓縮原文可以得到最短的編碼總長。然而哈夫曼編碼並不是唯一的。例如對字符串"aaaxuaxz",容易得到字母’a’、‘x’、‘u’、‘z’ 的出現頻率對應爲 4、2、1、1。我們可以設計編碼 {‘a’=0, ‘x’=10, ‘u’=110,‘z’=111},也可以用另一套 {‘a’=1, ‘x’=01, ‘u’=001, ‘z’=000},還可以用 {‘a’=0,‘x’=11, ‘u’=100, ‘z’=101},三套編碼都可以把原文壓縮到 14 個字節。但是 {‘a’=0, ‘x’=01,‘u’=011, ‘z’=001}就不是哈夫曼編碼,因爲用這套編碼壓縮得到 00001011001001後,解碼的結果不唯一,“aaaxuaxz” 和 "aazuaxax"都可以對應解碼的結果。本題就請你判斷任一套編碼是否哈夫曼編碼。

輸入格式:

首先第一行給出一個正整數 N(2≤N≤63),隨後第二行給出 N 個不重複的字符及其出現頻率,格式如下:
c[1] f[1] c[2] f[2] … c[N] f[N]
其中c[i]是集合{‘0’ - ‘9’, ‘a’ - ‘z’, ‘A’ - ‘Z’, ‘_’}中的字符;f[i]是c[i]的出現頻率,爲不超過 1000 的整數。再下一行給出一個正整數 M(≤1000),隨後是 M 套待檢的編碼。每套編碼佔 N 行,格式爲:
c[i] code[i]
其中c[i]是第i個字符;code[i]是不超過63個’0’和’1’的非空字符串。

輸出格式:

對每套待檢編碼,如果是正確的哈夫曼編碼,就在一行中輸出"Yes",否則輸出"No"。
注意:最優編碼並不一定通過哈夫曼算法得到。任何能壓縮到最優長度的前綴編碼都應被判爲正確。

輸入樣例:

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

輸出樣例:

Yes
Yes
No
No

這道題當時準備建立一個哈哈哈哈哈夫曼樹進行求解,但是想到代碼量就放棄了,因爲身爲ACMer我太懶了 要追求更好的解題策略!

代碼寫了100行左右(加上註釋就多了),我大概搜了搜其他人的基本上都是150+。。。

解題思路:

這道題主要有兩個部分,首先要求解出WPL最優編碼長度,和給定的編碼的總長度進行對比,如果給出的不是最優的直接結束。第二步是判斷給出的編碼有沒有公共前綴,沒有公共前綴纔是合格的哈哈哈哈哈夫曼編碼。

第一部分求解WPL暴力的方法(建樹)會浪費很大的空間和時間,我用了一個結構體List模擬一個子樹,我沒有保留樹的結構,而是用list儲存一個子樹的葉節點(即要編碼的字符),用一個變量儲存子樹根節點的權值(葉節點權值之和),然後模擬建哈哈哈哈哈夫曼樹的過程,過程中不斷更新每一個字符的深度(用數組保存)。

下面就是模擬過程(隨緣畫法),每一行是循環一次後優先隊列的內容,一個紅圈是一個子樹(結構體List,數字是根節點的權值)

建樹過程

#include <algorithm>
#include <iostream>
#include <string>
#include <queue>
#include <list>
using namespace std;

struct List//這是一個子樹
{
    list<char> l;//儲存子樹的葉節點(即編碼的字符)
    int p;//子樹祖先節點的權值(等於所有葉節點編碼長度之和)
    bool operator <(const List& a)const
    {
        //自定義優先級,權值小的子樹先出隊(堆),
        //若權值相同則葉節點多的子樹先出隊(爲減小總編碼長度)
        if(p==a.p)return l.size() < a.l.size();
        return p>a.p;
    }
};

bool cmp(string a, string b)
{
    //自定義字符串比較函數,用於排序給定的編碼01字符串
    //排序方式爲長度從大到小
    return a.size() > b.size();
}

bool prefix(string a, string b)
{
    //判斷兩個字符串是否有公共前綴
    int n = min(a.size(), b.size());
    for(int i = 0; i < n; i++)
        if(a[i] != b[i])
            return false;
    return true;
}

int main()
{
    priority_queue<List> que;//子樹(子節點)優先隊列
    int n, m, h[100] = {1}, p[100]; //h數組儲存每個字符的深度(即編碼長度)
                                    //p數組儲存每個字符的出現次數
                                    //最後用h[i]*p[i]求每個字符總編碼長度
    char c;
    cin >> n;
    for(int i = 0; i < n; i++)
    {
        List tmp;
        cin >> c >> tmp.p;
        p[c - 30] = tmp.p;//由於沒有重複的字符,c-30能保證不重複的記錄每一個字符
        tmp.l.push_back(c);
        que.push(tmp);
    }
    while(que.size() > 1)
    {
        List a = que.top();//取出當前優先級最高(權值最小的子樹)
        que.pop();
        List b = que.top();//取出另一個子樹
        que.pop();
        a.p += b.p;//兩個子樹進行合併
        for(auto i : a.l)
            h[i - 30]++;//給每一個子節點的深度加一
        for(auto i : b.l)
        {
            h[i - 30]++;
            a.l.push_back(i);//合併兩個子樹的葉節點(字符)
        }
        que.push(a);//將合併後的子樹入隊
    }

    int WPL = 0;//計算最優編碼長度

    List a = que.top();
    que.pop();
    for(auto i : a.l)//遍歷構造好的“樹”
        WPL += h[i - 30] * p[i - 30];//統計總編碼長度

    string str[100];//記錄給出的每個字符的編碼
    cin >> m;
    while(m--)//m組樣例
    {
        for(int i = 0; i < 100; i++)
            str[i] = "";//初始化
        int cnt = 0;//統計總編碼長度,最後和最優編碼長度進行比較
        for(int i = 0; i < n; i++)
        {
            cin >> c;
            cin >> str[c - 30];
            cnt += str[c - 30].size() * p[c - 30];//統計編碼長度
        }
        if(cnt > WPL)//如果編碼長度沒有達到最優直接輸出No
        {
            cout << "No" << endl;
            continue;
        }
        //下面進行判斷是否有公共前綴
        sort(str, str + 100, cmp);//按長度進行排序()

        bool flag = 1;
        for(int i = n - 1; i > 0; i--)//從最短的編碼開始與所有編碼進行比較
        {
            int j = i - 1;
            while(j >= 0)
            {
                if(prefix(str[i], str[j]))//若果有公共前綴結束循環
                {
                    flag = 0;
                    break;
                }
                j--;
            }
            if(flag == 0)
                break;
        }
        if(flag)
            cout << "Yes" << endl;
        else
            cout << "No" << endl;
    }

    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章