做[NOI2015]荷马史诗这道题的时候接触到了Huffman树,所以还是想写一篇学习笔记.
主要是(O(nlogn))和(O(N))构造(k)叉哈夫曼树.
定义:给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。---《某度某科》.
构造哈夫曼树,其实就是不停的“合并”的过程,并且每次合并,我们都是取前k个最小的数.算法的主要复杂就在于如何取前k个最小的数.
(O(nlogn))的话,可以是不停的sort排序,或者借助优先队列/堆等数据结构.以堆为例:
1.建立一个小根堆,插入N个叶子节点的权值
2.取堆顶的k个节点的权值,(ans+=w_1+w_2+...+w_k)
3.建立一个权值为(w_1+w_2+...+w_k)的树节点p,令p成为取出的k个点的父亲.
4.在堆中插入权值(w_1+w_2+...+w_k)
5.重复第2~4步,直至堆的大小为1.这个权值就是哈夫曼树根的权值.
最后,所有新建的树节点p与原来的叶子节点再加上最后剩下的树根构成的树就是哈夫曼树,变量ans就是最小带权路径长度(见上面的定义),题目往往要求的就是这个ans.
值得一提的是,我们每次要在堆中取出k个值,而有可能最后一次不足以取出k个,因此我们在执行上述算法之前,补加一些额外权值为0的叶子节点放入堆中,使叶子节点的个数n满足((n-1) mod (k-1)=0),这样就能保证每次取k个且不影响贪心的正确性.
priority_queue<int,vector<int>,greater<int> >q;
inline void Huffman(int n,int k){
int ans=0;
while(n>1){
int tot=0;
for(int i=1;i<=k;++i){
tot+=q.top();q.pop();
}
q.push(tot);
ans+=tot;n-=k-1;
}
printf("%d
",ans);
}
int main(){
int n=read(),k=read();
for(int i=1;i<=n;++i){
int val=read();
q.push(val);
}
int res=(n-1)%(k-1);
if(res!=0)res=k-1-res,n+=res;
for(int i=1;i<=res;++i)q.push(0);
Huffman(n,k);
return 0;
实际上我们借助归并排序的思想来排序可以把总的时间复杂度优化到真正意义上的(O(nlogn)).
1.先将n个数从小到大sort排序一次
2.取k个数合并成一个新的数放入数组b的末尾
3.之后,每次从a,b两个数组里挑选出k个最小的数合并再次放到数组b的末尾
考虑第3步如何取出k个最小的数,注意到b数组是单调递增的.因为每次放入的都是当前最小的k个数之和,所以后面放的肯定比前面的大.
因为a数组已经sort排序过了,那么也就是a,b数组都是单调递增的,所以只要分别维护一个指针,起初都指向数组的第一个位置,然后每次比较谁小就取谁,被取的那个指针往后移一位即可.
4.重复第3步,直到a数组为空,b数组只剩一个数(这个数就是树根的权值)
同样地,每次我们都是取k个数,但在最后一次取的时候,可能已经不足k个数可取了,所以不妨在算法的开始,我们就先取掉x个数,使得剩下的n-x个数正好能每次取k个取完.
inline void Huffman(int n,int k){
int ai=0,bi=0,blen=0,ans=0;
bool first=true;
while(n-ai+blen-bi>1){
int num=0;
if(first){
if((n-k)%(k-1)==0)num=k;
else num=(n-k)%(k-1)+1;
first=false;
}
else num=k;
int sum=0;
while(num--){
if(ai==n)sum+=b[bi],bi++;
else if(bi==blen)sum+=a[ai],ai++;
else if(a[ai]<b[bi])sum+=a[ai],ai++;
else sum+=b[bi],bi++;
}
ans+=sum;b[blen++]=sum;
}
printf("%d
",ans);
}
int main(){
int n=read(),k=read();
for(int i=0;i<n;++i)a[i]=read();
sort(a+1,a+n);
Huffman(n,k);
return 0;
}
总结一下,个人认为第一种堆的做法虽然慢一些,但是更好理解,更好写,也更好用。这一点在荷马史诗中有很好的体现,因为这道题不仅要求ans,还要求另外的东西,这样借助堆我们可以不仅仅只维护权值,还可以维护深度等其他信息.