1. 程式人生 > >Huffman樹與Huffman編碼—介紹與基本應用

Huffman樹與Huffman編碼—介紹與基本應用

今天來談談huffman樹吧。

先介紹一下樹的路徑長度(path length of a tree,PL),和樹的帶權路徑長度(Weighted Path Length of Tree,WPL)。我們定義每個節點到樹根的距離為l[i]。
樹的路徑長度(PL):所有節點到根的距離之和。\sum l[i]
樹的帶權路徑長度(WPL):所有節點到根的距離與權值的乘積之和。\sum l[i]*w[i]

Huffman樹可以是k叉的。我們先從最簡單的二叉huffman樹來學習。uffman樹可以是k叉的。我們先從最簡單的二叉huffman樹來學習。

構造PL最小的huffman樹

問題:給出n個原節點,求其構造成一棵有最小的PL的二叉樹。
原理:將huffman樹構造成一棵完全二叉樹。
我們記錄每個節點的當前深度(即合併次數)和其包含的原節點(這兩個其實是等價的),把所有節點的深度放入小根堆中,每次取出最小的兩個,將其合併。合併時,新節點的合併次數+1,原節點樹等於左右子樹的原節點樹之和,ans=ans+左邊的原節點數+右邊的原節點樹=ans+新節點的原節點數。最後的ans就是最小的PL了。

構造WPL最小的huffman樹

問題:給出n個原節點,每個節點有一個權值w[i],求其構造成一棵有最小的WPL的二叉樹。
原理:把權值大的原節點放在深度小的地方,權值小的放在深度大的地方。
把所有節點的權值放入小根堆,每次取出兩個權值最小的節點,將其合併。合併時,新節點的權值等於左右節點的權值之和,ans=ans+左邊的權值+右邊的權值=ans+新節點的權值。最後的ans也就是最小的WPL。

問題升級,現在要構造k叉的huffman樹

基本的思路是不變的,只是構造時要選前k小的節點來合併。但是要注意,如果這麼從下往上做,到根節點是,根節點的子節點可能不足k個,這樣顯然不是最優解。於是我們要補上一些沒有影響的0節點,使得樹的節點數滿足(n-1)%(k-1)==0。這樣做後再取前k小的節點合併就是正確的。


例題1 洛谷1090 合併果子

【題目】

在一個果園裡,多多已經將所有的果子打了下來,而且按果子的不同種類分成了不同的堆。多多決定把所有的果子合成一堆。
每一次合併,多多可以把兩堆果子合併到一起,消耗的體力等於兩堆果子的重量之和。可以看出,所有的果子經過 n−1 次合併之後, 就只剩下一堆了。多多在合併果子時總共消耗的體力等於每次合併所耗體力之和。
因為還要花大力氣把這些果子搬回家,所以多多在合併果子時要儘可能地節省體力。假定每個果子重量都為 1 ,並且已知果子的種類 數和每種果子的數目,你的任務是設計出合併的次序方案,使多多耗費的體力最少,並輸出這個最小的體力耗費值。
例如有3種果子,數目依次為 1,2,9。可以先將1 、2 堆合併,新堆數目為3,耗費體力為3。接著,將新堆與原先的第三堆合併,又得到新的堆,數目為 12 ,耗費體力為 12 。所以多多總共耗費體力 =3+12=15 。可以證明 15 為最小的體力耗費值。

【題解】
把n堆兩堆兩堆地合成一堆,把其過程畫出來,不就是二叉huffman樹嗎?它要求的是最小的WPL。

【程式碼】

#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int inf=10010;

int n;
priority_queue<int,vector<int>,greater<int> > q;//小根堆

int main()
{
    scanf("%d",&n);
    for(int i=1,x;i<=n;i++)
    {
        scanf("%d",&x);
        q.push(x);
    }
    int ans=0;
    for(int i=1;i<n;i++)
    {
        int t1=q.top();q.pop();//每次取出最小的兩個,將其合併
        int t2=q.top();q.pop();
        ans+=t1+t2;
        q.push(t1+t2);
    }
    printf("%d\n",ans);
    return 0;
}

Huffman編碼

問題:一篇電文,原文為AMCADEDDMCCAD。現在要把原文轉換成01串傳送給對方。為了節省資源,我們當然希望翻譯好的01串長度儘量的短。怎麼辦?
我們發現原文只有E,M,C,A,D,五個字母出現的次數分別為1,2,3,3,4。我們以此為權值,構建一棵WPL最小的huffman樹。

我們給左節點配碼0,右節點配碼1。從根到該字母的路徑上的程式碼連起來,就是該字母的huffman編碼:
E(000),M(001),C(01),A(10),D(11)
我們發現huffman編碼的字首是不同的。把huffman樹看成trie樹,因為所有的字母都在葉子節點,沒有一個字母B在另一個字母A從根到葉子A的路徑上,所以即使有相同部分,它B一定不會是A的字首。
各字母的編碼即為哈夫曼編碼: EMCAD 所有編碼長度和為12位,即PL=12,此時的PL並不是最小的,但此時的WPL一定是最小的。WPL最小才能使得密報翻譯的01串長度最短。
原電文AMCADEDDMCCAD翻譯成01串後為:10001011011000111100101011011。
我們對其翻譯會原文,試一試,只有一種翻譯方法。沒有一個01串會翻譯成多個字母串,這就是相互不是字首的作用。


【例題2】洛谷2168 荷馬史詩

【題目】
追逐影子的人,自己就是影子 ——荷馬

Allison 最近迷上了文學。她喜歡在一個慵懶的午後,細細地品上一杯卡布奇諾,靜靜地閱讀她愛不釋手的《荷馬史詩》。但是由《奧德賽》和《伊利亞特》 組成的鴻篇鉅製《荷馬史詩》實在是太長了,Allison 想通過一種編碼方式使得它變得短一些。
一部《荷馬史詩》中有n種不同的單詞,從1到n進行編號。其中第i種單 詞出現的總次數為wi。Allison 想要用k進位制串si來替換第i種單詞,使得其滿足如下要求:
對於任意的 1 ≤ i, j ≤ n , i ≠ j ,都有:si不是sj的字首。
現在 Allison 想要知道,如何選擇si,才能使替換以後得到的新的《荷馬史詩》長度最小。在確保總長度最小的情況下,Allison 還想知道最長的si的最短長度是多少?
一個字串被稱為k進位制字串,當且僅當它的每個字元是 0 到 k − 1 之間(包括 0 和 k − 1 )的整數。
字串 str1 被稱為字串 str2 的字首,當且僅當:存在 1 ≤ t ≤ m ,使得str1 = str2[1..t]。其中,m是字串str2的長度,str2[1..t] 表示str2的前t個字元組成的字串。

【題解】
好複雜的題目啊,又是k進位制,又是字首,還有出現次數,完全混亂不知所措。但是一往huffman編碼想,發現整體都在圍繞huffman編碼的基本要求,所有條件都在限制,讓它可以是huffman編碼。
k進位制的含義是k叉樹。出現次數的含義是每個節點的權值。
題目中提到,“在確保總長度最小的情況下,Allison 還想知道最長的si的最短長度是多少?”,用huffman術語來說:“在確保WPL最小的情況下,讓huffman樹的深度最小”。我們應對的策略是,當權值相同時,優先讓當前深度小的(合併次數少的)先合併,因為每次合併都會增加1的深度,這麼做可以使樹儘可能的平衡,最大深度也會較小。

【程式碼】

#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e5+10;

ll n,k;
struct N
{
    ll x,m;//x權值,m合併次數 
    bool operator<(N n1) const
    {
        if(x!=n1.x) return x>n1.x;
        return m>n1.m;//在權值相同時,合併次數少的優先 
    }
};priority_queue<N> q;

int main()
{
    scanf("%lld%lld",&n,&k);
    for(int i=1;i<=n;i++)
    {
        ll x;
        scanf("%lld",&x);
        q.push((N){x,0});
    }
    if((n-1)%(k-1)!=0)//補上0節點 
        for(int i=(n-1)%(k-1)+1;i<k;i++) q.push((N){0,0}),n++;
    
    ll ans1=0,ans2=0;
//  for(int i=1;i<n;i+=k-1)
    while(q.size()!=1)//結束標誌有兩種方法 
    {
        N now=(N){0,0};
        for(int j=1;j<=k;j++)//取出前k小的節點 
        {
            now.x+=q.top().x;
            now.m=max(now.m,q.top().m);
            q.pop();
        }
        now.m++;
        q.push(now);
        ans1+=now.x;//ans1記錄長度 
        ans2=max(ans2,now.m);//ans2記錄最大深度 
    }
    printf("%lld\n%lld\n",ans1,ans2);
    return 0;
}