前言
我們知道,要構造Huffman Tree,每次都要從堆中彈出最小的兩個權重的節點,然後把這兩個權重的值相加存放到新的節點中,同時讓這兩個節點分別成為新節點的左右兒子,再把新節點插入到堆中。假設節點個數為n,則重複n-1次後,最後堆中的那個節點就是Huffman Tree的根。
用堆實現當然可以,但是比較麻煩。你需要定義一個最小堆,堆的初始化操作,堆的插入操作,取出最小元素並調整堆的操作。先不說對這些程式碼是否熟悉掌握,當把這些函式都碼完,別人題目都已經做完了。
這裡我們用更方便的方法來構造一顆Huffman Tree。就是用STL中的優先佇列。其實優先佇列的本質就是一個堆。這樣我們就不需要再動手碼這麼多的函式了。同時,如果以後的題目需要用到堆這種資料結構,直接用優先佇列就可以了。
用優先佇列構造Huffman Tree
要使用優先佇列 priority_queue ,就需要包含標頭檔案 #include <queue> 。
樹節點的定義如下:
1 struct Data { 2 char letter; 3 int freq; 4 }; 5 6 struct TNode { 7 Data data; 8 TNode *left, *right; 9 };
然後輸入字元和頻率大小,把TNode*壓入到優先佇列中。當我們需要頻率最小的頻率的那個節點,只需要從優先佇列中彈出一個元素就可以了,那個元素就是含有最小頻率的那個節點。
不過需要注意的是,優先佇列預設情況下是一個最大堆,這需要我們自定義一個比較函式,以實現最小堆。同時,我們比較的資料型別是我們自定義的資料型別TNode*,所以需要改成相應的資料型別。
這裡我們通過重寫仿函式,來實現最小堆:
1 class cmp { 2 public: 3 bool operator()(TNode *a, TNode *b) { 4 // 當返回true,說明a的優先順序小於b 5 // 這裡用 '>' 表示,如果a節點對應的頻率大於b節點,就說明a的優先順序小於b,從而實現堆頂元素是頻率最小的那個節點,也就是最小堆 6 return a->data.freq > b->data.freq; 7 } 8 };
這裡我們壓入到優先佇列中的資料型別是TNode*,因此在定義優先佇列時,傳入的資料型別是TNode*。讀入資料的函式如下:
1 void readData(std::priority_queue<TNode*, std::vector<TNode*>, cmp> &pq) { // 傳入在main函式中定義的優先佇列 2 int n; 3 scanf("%d", &n); // 輸入節點的個數 4 for (int i = 0; i < n; i++) { 5 TNode *tmp = new TNode; 6 7 getchar(); // 把多餘的字元,也就是回車和空格讀掉 8 scanf("%c %d", &tmp->data.letter, &tmp->data.freq); 9 tmp->left = tmp->right = NULL; 10 pq.push(tmp); // 把新節點的指標壓進優先佇列中 11 } 12 }
最後是核心程式碼,構造Huffman Tree的函式。
函式框架:如果優先佇列不為空,則新建一個TNode節點,彈出堆頂的元素,並讓新節點的left指向彈出這個彈出的節點。再判斷一次優先佇列是否為空;
- 如果不為空,就再彈出一個堆頂元素,並讓新節點的right指向彈出的節點。同時,把彈出的兩個節點的頻率相加的結果存放到新節點中,最後把新節點的指標壓到堆中。
- 如果為空,就說明剛剛彈出的節點就是我們要構造的Huffman Tree的根節點,只需要把它返回就可以了。
所以,構造Huffman Tree的函式如下:
1 TNode *createHuffmanTree(std::priority_queue<TNode*, std::vector<TNode*>, cmp> &pq) { 2 while (!pq.empty()) { // 優先佇列不為空 3 TNode *tmp = new TNode; // 新建一個節點 4 tmp->left = pq.top(); // 彈出堆頂元素,作為新節點的左孩子 5 pq.pop(); 6 7 if (!pq.empty()) { // 剛才彈出元素後,優先佇列不為空 8 tmp->right = pq.top(); // 再彈出一個元素,作為新節點的右孩子 9 pq.pop(); 10 11 tmp->data.freq = tmp->left->data.freq + tmp->right->data.freq; // 把左右孩子存放的頻率的相加結果存放到新節點中 12 pq.push(tmp); // 把新節點的指標壓入優先佇列中 13 } 14 else { // 否則,剛才彈出元素後,優先佇列就空了 15 return tmp->left; // 剛才彈出的元素就是Huffman Tree的根節點,直接返回即可 16 } 17 } 18 }
現在給出完整的構造Huffman Tree的程式碼,同時計算出這顆Huffman Tree的WPL。
1 #include <cstdio> 2 #include <queue> 3 #include <vector> 4 5 struct Data { 6 char letter; 7 int freq; 8 }; 9 10 struct TNode { 11 Data data; 12 TNode *left, *right; 13 }; 14 15 class cmp { 16 public: 17 bool operator()(TNode *a, TNode *b) { 18 return a->data.freq > b->data.freq; 19 } 20 }; 21 22 void readData(std::priority_queue<TNode*, std::vector<TNode*>, cmp> &pq); 23 TNode *createHuffmanTree(std::priority_queue<TNode*, std::vector<TNode*>, cmp> &pq); 24 int WPL(TNode *T, int depth); 25 26 int main() { 27 std::priority_queue<TNode*, std::vector<TNode*>, cmp> pq; 28 29 readData(pq); 30 TNode *huffmanTree = createHuffmanTree(pq); 31 printf("%d", WPL(huffmanTree, 0)); 32 33 return 0; 34 } 35 36 void readData(std::priority_queue<TNode*, std::vector<TNode*>, cmp> &pq) { 37 int n; 38 scanf("%d", &n); 39 for (int i = 0; i < n; i++) { 40 TNode *tmp = new TNode; 41 42 getchar(); 43 scanf("%c %d", &tmp->data.letter, &tmp->data.freq); 44 tmp->left = tmp->right = NULL; 45 pq.push(tmp); 46 } 47 } 48 49 TNode *createHuffmanTree(std::priority_queue<TNode*, std::vector<TNode*>, cmp> &pq) { 50 while (!pq.empty()) { 51 TNode *tmp = new TNode; 52 tmp->left = pq.top(); 53 pq.pop(); 54 55 if (!pq.empty()) { 56 tmp->right = pq.top(); 57 pq.pop(); 58 59 tmp->data.freq = tmp->left->data.freq + tmp->right->data.freq; 60 pq.push(tmp); 61 } 62 else { 63 return tmp->left; 64 } 65 } 66 } 67 68 int WPL(TNode *T, int depth) { 69 if (T->left == NULL && T->right == NULL) return depth * T->data.freq; 70 else return WPL(T->left, depth + 1) + WPL(T->right, depth + 1); 71 }
對應的Huffman Tree如下,通過檢驗WPL正是77。
滿足最優編碼的條件
我們知道,通過構造Huffman Tree而得到的編碼一定是最優編碼,但是最優編碼不一定是通過構造Huffman Tree來得到的。而且通過構造Huffman Tree得到的最優編碼是不唯一的,任意交換左右子樹的位置得到的也是最優編碼。
所以我們如何判斷給定的編碼是否為最優編碼?首先,我們要找到最優編碼的共同特點:
- 最優編碼的WPL一定是最小的。
- 無歧義解碼——字首碼:資料僅存於葉子節點。
- 沒有度為1的節點。
其中如果滿足1,2這兩個條件,就一定滿足第3個條件,這個可以用反證法證明。所以,要判斷編碼是否為最優編碼,只需要檢驗編碼是否滿足1,2這兩個條件就可以了。
下面給出一道具體的題目,來說明如何對編碼進行1,2點的檢驗。
判斷編碼是否為最優編碼
這裡給出一道例題:Huffman Codes。題目就是給定一組字元的頻率,再給出多組字元對應的編碼,讓我們來判斷這些編碼是否為最優編碼。
原題以及更詳細的題解可以參考:https://www.cnblogs.com/onlyblues/p/14628257.html,這裡給出多種解法,有用堆去實現的,有用優先佇列實現的。
這裡我們用優先佇列來判斷編碼是否為最優編碼。我們只摘取題目中的測試樣例:
Sample Input:
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
Sample Output:
Yes
Yes
No
No
雖然題目看上去好像要構造一顆Huffman Tree,但實際上我們可以在整一個過程中不構造任何一顆樹,只需要用到STL中的map和priority_queue就可以完成判斷編碼是否為最優編碼。我們只需要判斷編碼是否為最優編碼,因此更多的是處理頻率這一資料。
先給出整一個程式框架。我們先讀入字元和對應頻率,同時把頻率壓入優先佇列形成最小堆。然後再輸入多組要判斷是否為最優編碼的資料,同時進行相應的判斷和檢測。所以我們的main函式的框架就是這樣:
1 int main() { 2 map<char, int> letterFreq; // 用map來儲存字元和對應的頻率,字元對映為對應的頻率 3 priority_queue< int, vector<int>, greater<int> > pq; // 優先佇列,儲存的資料型別為int,由於預設是最大堆,所以傳入greater<int>使其變成最小堆 4 5 int n; 6 cin >> n; 7 readLetterFreq(letterFreq, pq, n); // 讀入字元和頻率,同時為頻率生成最小堆的函式 8 checkOptimalCode(letterFreq, pq, n); // 判斷多組編碼是否為最優編碼的函式 9 10 return 0; 11 }
下面來分析這兩個函式是如何實現的。首先,對於輸入的字元和對應的頻率,我們用map來儲存,形成一種對映的關係。同時在輸入字元和頻率的過程中,我們把頻率壓入優先佇列中,這樣就可以在讀入字元和頻率的過程中,也完成最小堆的構造。注意,我們壓入優先佇列的是字元頻率,所以優先佇列儲存的資料型別是int,而不再是上面的TNode*了。
readLetterFreq函式相關程式碼如下:
1 void readLetterFreq(map<char, int> &letterFreq, priority_queue< int, vector<int>, greater<int> > &pq, int n) { 2 for (int i = 0; i < n; i++) { 3 char letter; 4 getchar(); // 讀掉多餘的字元,如回車,空格 5 cin >> letter; // 讀入字元 6 getchar(); 7 cin >> letterFreq[letter]; // 讀入頻率,為字元的對映 8 9 pq.push(letterFreq[letter]);// 把讀入的頻率壓入到優先佇列中,構成最小堆 10 } 11 }
接下來我們要做的事情是計算給定字元的WPL。其實計算WPL不一定要構造一顆Huffman Tree,然後用深度乘以頻率再求和來得到。還有一種方法是把Huffman Tree中度為2的節點存放的頻率都相加起來,最後得到的結果也是WPL。這是因為葉子節點被重複計算,和用深度乘以頻率的原理基本一樣。就拿題目給的測試樣例來舉例:
這看上去還是要構造一顆樹啊,但實際上,如果我們用優先佇列根本不需要構造一顆樹。思路是這樣的:我們要有一個變數來累加如上圖度為2節點存放頻率。每次從優先佇列裡彈出兩個頻率,這兩個頻率是優先佇列中所包含頻率裡面最小的那兩個,然後把這兩個頻率相加,相加的結果其實就對應上圖度為2節點存放的頻率,也就是紅色的數字。然後把相加的結果累加到一個變數,同時把相加的結果壓入優先佇列中。其實這個累加的過程就是累加上圖紅色的那些數字。一直重複,直到優先佇列為空,那麼那個變數最後累加的結果就是我們要計算的WPL。
計算WPL的函式程式碼如下:
1 int getWPL(priority_queue< int, vector<int>, greater<int> > &pq) { 2 int wpl = 0; // 用來儲存累加的結果 3 while (!pq.empty()) { // 當優先佇列不為空 4 int tmp = pq.top(); // 從優先佇列彈出一個元素,這個元素就是最小頻率 5 pq.pop(); 6 7 if (pq.empty()) break; // 如果彈出那個頻元素優先佇列就為空了,退出迴圈 8 9 tmp += pq.top(); // 如果優先佇列不為空,再彈出一個元素,同時把兩個頻率進行相加 10 pq.pop(); 11 pq.push(tmp); // 把兩個頻率相加的結果壓入優先佇列中 12 13 wpl += tmp; // 同時,把這個相加結果進行累加,對應著累加度為2節點的存放頻率 14 } 15 16 return wpl; 17 }
接下來我們需要對多組編碼進行檢驗。即先檢驗編碼的長度是否與給定字元頻率的WPL相同,再檢驗是否為字首碼。
計算每組編碼的方法很簡單,由於輸入已經給出每個字元的編碼,所以就自然知道這個字元對應編碼的長度。所以並不需要呼叫上面的getWPL函式,只需要用這個字元對應的編碼長度乘以對應的頻率就可以了。每一組編碼的WPL計算公式為:
再判斷codeLen是否與上面求出的給定頻率的WPL相等,如果不相等,就說明這個編碼不是最優編碼,就不需要再判斷是否為字首碼了。如果相等再去判斷是否為字首碼。
這裡還有個陷阱。首先我們要知道,一個最優編碼的長度是不會超過n-1的。所以如果某個編碼的長度大於n-1也說明該編碼不是最優編碼。
這裡先給出checkOptimalCode函式的程式碼,接下來解釋如何判斷編碼是否為字首碼。
1 void checkOptimalCode(map<char, int> &letterFreq, priority_queue< int, vector<int>, greater<int> > &pq, int n) { 2 int wpl = getWPL(pq); // 用不構造Huffman Tree的方法來計算WPL 3 4 int m; 5 cin >> m; // 輸入判斷編碼的組數m 6 for (int i = 0; i < m; i++) { 7 string code[n]; 8 int codeLen = 0; 9 bool ret = true; 10 11 for (int i = 0; i < n; i++) { 12 char letter; 13 getchar(); 14 cin >> letter >> code[i]; // 讀入字元和對應的編碼 15 16 if (ret) { // 如果已經知道該組編碼不是最優編碼就不需要再計算編碼長度了,但仍要繼續輸入 17 if (code[i].size() > n - 1) ret = false; // 如果某個字元的編碼長度大於n-1,說明該組編碼不是最優編碼 18 codeLen += code[i].size() * letterFreq[letter]; // 計算編碼長度 19 } 20 } 21 22 if (ret && codeLen == wpl) { // 如果ret == true並且編碼長度與WPL相同,才判斷該組編碼是否為字首碼 23 for (int i = 0; i < n; i++) { // 每個字元都跟它之後的字元進行判斷是否滿足字首碼的要求 24 for (int j = i + 1; j < n; j++) { 25 // 判斷某個編碼是否與另外一個編碼前m個位置的相同,詳細請看圖片 26 if (code[i].substr(0, code[j].size()) == code[j].substr(0, code[i].size())) { 27 ret = false; // 只要有一對編碼的字首相同,就說明這組的編碼不滿足字首碼 28 break; // 後面的字元不需要判斷了,直接退出退出判斷字首碼的迴圈 29 } 30 } 31 if (ret == false) break; 32 } 33 } 34 else { 35 ret = false; 36 } 37 38 cout << (ret ? "Yes\n" : "No\n"); 39 } 40 }
下面來說說如何判斷編碼是否為字首碼。首先,假設現在有兩個編碼,如果這兩個編碼不滿足字首碼的話,比如"110"和"1101",那麼其中一個編碼會與另外一個編碼前的m個位置的相同(其中m是指這兩個編碼長度中最小的那個長度)。也就是說"110",與"1101"的前3個位置的"110"相同,就說明"110"和"1101"不滿足字首碼。
我們需要對同組編碼的每兩個字元進行比較,需要比較的次數為 C(n, 2) = n * (n - 1) / 2 。
相關的函式程式碼上面已經給出。主要是 code[i].substr(0, code[j].size()) == code[j].substr(0, code[i].size()) 這個部分。
code[i].substr(0, code[j].size()) == code[j].substr(0, code[i].size()) ,這麼做始終能夠保證取到兩個編碼中,長度最小那個編碼的全部,以及另外一個編碼的前面同樣長度的部分,來進行判斷是否滿足字首碼。
下面給出這道題完整的AC程式碼:
1 #include <cstdio> 2 #include <iostream> 3 #include <string> 4 #include <vector> 5 #include <queue> 6 #include <map> 7 using namespace std; 8 9 void readLetterFreq(map<char, int> &letterFreq, priority_queue< int, vector<int>, greater<int> > &pq, int n); 10 void checkOptimalCode(map<char, int> &letterFreq, priority_queue< int, vector<int>, greater<int> > &pq, int n); 11 int getWPL(priority_queue< int, vector<int>, greater<int> > &pq); 12 13 int main() { 14 map<char, int> letterFreq; 15 priority_queue< int, vector<int>, greater<int> > pq; 16 17 int n; 18 cin >> n; 19 readLetterFreq(letterFreq, pq, n); 20 checkOptimalCode(letterFreq, pq, n); 21 22 return 0; 23 } 24 25 void readLetterFreq(map<char, int> &letterFreq, priority_queue< int, vector<int>, greater<int> > &pq, int n) { 26 for (int i = 0; i < n; i++) { 27 char letter; 28 getchar(); 29 cin >> letter; 30 getchar(); 31 cin >> letterFreq[letter]; 32 33 pq.push(letterFreq[letter]); 34 } 35 } 36 37 void checkOptimalCode(map<char, int> &letterFreq, priority_queue< int, vector<int>, greater<int> > &pq, int n) { 38 int wpl = getWPL(pq); 39 40 int m; 41 cin >> m; 42 for (int i = 0; i < m; i++) { 43 string code[n]; 44 int codeLen = 0; 45 bool ret = true; 46 47 for (int i = 0; i < n; i++) { 48 char letter; 49 getchar(); 50 cin >> letter >> code[i]; 51 52 if (ret) { 53 if (code[i].size() > n - 1) ret = false; 54 codeLen += code[i].size() * letterFreq[letter]; 55 } 56 } 57 58 if (ret && codeLen == wpl) { 59 for (int i = 0; i < n; i++) { 60 for (int j = i + 1; j < n; j++) { 61 if (code[i].substr(0, code[j].size()) == code[j].substr(0, code[i].size())) { 62 ret = false; 63 break; 64 } 65 } 66 if (ret == false) break; 67 } 68 } 69 else { 70 ret = false; 71 } 72 73 cout << (ret ? "Yes\n" : "No\n"); 74 } 75 } 76 77 int getWPL(priority_queue< int, vector<int>, greater<int> > &pq) { 78 int wpl = 0; 79 while (!pq.empty()) { 80 int tmp = pq.top(); 81 pq.pop(); 82 83 if (pq.empty()) break; 84 85 tmp += pq.top(); 86 pq.pop(); 87 pq.push(tmp); 88 89 wpl += tmp; 90 } 91 92 return wpl; 93 }
參考資料
Huffman Codes:https://www.cnblogs.com/onlyblues/p/14628257.html
priority_queue的用法:https://www.cnblogs.com/Deribs4/p/5657746.html