用優先佇列構造Huffman Tree及判斷是否為最優編碼的應用

onlyblues 發表於 2021-04-08

前言

  我們知道,要構造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及判斷是否為最優編碼的應用

  對應的Huffman Tree如下,通過檢驗WPL正是77。

用優先佇列構造Huffman Tree及判斷是否為最優編碼的應用

 

滿足最優編碼的條件

  我們知道,通過構造Huffman Tree而得到的編碼一定是最優編碼但是最優編碼不一定是通過構造Huffman Tree來得到的。而且通過構造Huffman Tree得到的最優編碼是不唯一的,任意交換左右子樹的位置得到的也是最優編碼。

用優先佇列構造Huffman Tree及判斷是否為最優編碼的應用

  所以我們如何判斷給定的編碼是否為最優編碼?首先,我們要找到最優編碼的共同特點:

  1. 最優編碼的WPL一定是最小的。
  2. 無歧義解碼——字首碼:資料僅存於葉子節點。
  3. 沒有度為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。這是因為葉子節點被重複計算,和用深度乘以頻率的原理基本一樣。就拿題目給的測試樣例來舉例:

用優先佇列構造Huffman Tree及判斷是否為最優編碼的應用

  這看上去還是要構造一顆樹啊,但實際上,如果我們用優先佇列根本不需要構造一顆樹。思路是這樣的:我們要有一個變數來累加如上圖度為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計算公式為:用優先佇列構造Huffman Tree及判斷是否為最優編碼的應用

  再判斷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()) 這個部分。

用優先佇列構造Huffman Tree及判斷是否為最優編碼的應用

   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 Tree及判斷是否為最優編碼的應用

 

參考資料

  Huffman Codes:https://www.cnblogs.com/onlyblues/p/14628257.html

  priority_queue的用法:https://www.cnblogs.com/Deribs4/p/5657746.html