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;
}