带权路径:是树中所有的叶结点的权值乘上其到根结点的路径长度。
哈夫曼树就是带权路径最小的树。
有n个数(即n个叶子节点),构造k叉(k>=2)哈夫曼树的方法;
构造哈夫曼树,其实就是不停的“合并”的过程。并且每次合并,我们都是取前k个最小的数。想的到,算法的主要复杂就在于如何取前k个最小的数;不停排序或者优先队列、堆都可以做到,但复杂都接nlogn,那么我们可不可以只排序一次就能每次都取出前k个数呢?
答案是能!
我们可以维护两个数组利用类似与“归并排序”的想法,O(n)的构造出k叉哈夫曼树。
第一步:先将n个数从小到大排序一次,放入第一个数组a;取k个数合并成一个新的数放入数组b的末尾。
之后,每次从两个数组里挑选出k个最小的数合并再次放到数组b的末尾。
这里不难发现:b数组是有序的。因为每次放入的都是最小的k个数之和,第二次放的肯定比第一次大。
那么也就是a,b数组都是有序的,所以之前说的从两个数组里取k个最小的数出来也就不难做到,只要维护两个指针,都指向数组的第一个位置,然后每次比较谁小就取谁,被取的那个指针往后移一位。
最后,当a数组为空,b数组只剩一个数时算法结束。(这个数即哈夫曼的根)
在以上的描述中,为了方便我省略了一个细节:
注意到,每次我们都是取k个数,但在最后一次取的时候,可能已经不足k个数可取了,所以不妨在算法的开始,我们就先取掉x个数,使得剩下的n-x个数正好能每次取k个取完。
算法总的来说,就是两个指针从首扫到尾,时间复杂度不超过O(2*n),已经是非常优秀。
关于k叉哈夫曼树在ACM中的最常用的应用无疑就是求合并n个数最小的代价。
像
HDU 5884
4198: [Noi2015]荷马史诗
给一个模版
int Hafuman(int k) //返回总代价
{
int ai, bi, blen;
blen = 0;
ai = bi = 0;
int cost = 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++;
}
}
cost += sum;
b[blen++] = sum;
}
return cost;
}