双有序队列算法——处理哈夫曼K叉树的高效算法
算法介绍:
哈夫曼树的思路及实现众所周知,大部分是用堆来维护和实现,这种思路比较清晰,在K比较小的时候处理较快(具体例子接下来再说),而且编程复杂度不是很高,利于应用。但是,其所用的数据结构是树,是在一个森林中操作。有没有更简单的结构?下面介绍一个用一位数组维护的算法——双有序队列算法。
例题分析:
先看一个简单的例子:NOIP2004合并果子
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
例如有3种果子,数目依次为1,2,9。可以先将1、2堆合并,新堆数目为3,耗费体力为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为12。所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。
输入文件fruit.in包括两行,第一行是一个整数n(1<=n<=10000),表示果子的种类数。第二行包含n个整数,用空格分隔,第i个整数ai(1<=ai<=20000)是第i种果子的数目。
输出文件fruit.out包括一行,这一行只包含一个整数,也就是最小的体力耗费值。输入数据保证这个值小于231。 思路分析:
很明显,这可以用哈夫曼二叉树解决。拿堆来维护是普遍选择。经过测试,其总耗时为0.02s左右。下面以此为例,介绍一下双有序队列算法的原理和实现。哈夫曼树的思想是贪心,则每次需要取一定量的最小值来处理。普通的排序效率一般都不错,但是会被过大的N给卡住。所以很多人用堆排。堆排虽然处理的较快,但是需要反复维护,可以看做是在反复排序,只是代价很小罢了。有没有可以一劳永逸的算法?有!用归并排序实现,只排一遍.开两个数组,将第一次拍好的数放进去,记录好每个数组的长度和头部位置,然后,进行贪心,从两个队列中选取2个(或K个)最小值,记录消耗,进行合并,放到第二个队列尾部,分别处理每个数组的长度和头部位置。以此类推……直到只剩下一个元素为止即可。证明太简单,略掉。
代码讲解:
核心伪代码:
一.取数阶段:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
q:=0;
w:=0; // q,w为记录的每个队列取了几个元素,d为两个队列,0位存放的是长度,t1,t2为队列头元素位置
while (q+w<2) and (q<d[1,0]) and (w<d[2,0]) do //在判断取够了没有 的同时,也要判断是否溢出
if d[1,q+t1+1]<=d[2,w+t2+1] then inc(q) else inc(w); // 进行贪心选择取数
if q=d[1,0] then if q+w<2 then w:=w+(2-q);
if w=d[2,0] then if q+w<2 then q:=q+(2-w); // 如果因溢出而跳出,补上是很有必要的
二.处理阶段(队列为空的转移):
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
if d[1,0]=0 then begin // 要保证第一个队列不能为空,第一个是主队列,第二个可以理解为暂存的队列
for i:=t2+1 to d[2,0]+t2 do
begin
d[1,i-t2]:=d[2,i];
d[2,i]:=0;
end;
d[1,0]:=d[2,0];
d[2,0]:=0;
t1:=0; t2:=0;
end; // 具体的转移不再解释,总之要清理干净
三.计算阶段:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
d[1,0]:=d[1,0]-q; // 去掉取走的数
h:=0;
for i:=t1+1 to t1+q do
begin
h:=h+d[1,i];
d[1,i]:=0;
end;
for i:=t2+1 to t2+w do
begin
h:=h+d[2,i];
d[2,i]:=0;
end; // 将总和累积起来
ans:=ans+h;
d[2,d[2,0]+t2+1]:=h; // 将新出现的元素存放好
d[2,0]:=d[2,0]+1-w; // 处理长度
t1:=t1+q;
t2:=t2+w; // 处理指针
完整代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
program zht;
var
i,n,h,q,w,t1,t2:longint;
ans:int64;
d:array[1..2,0..20000] of longint;
a,a2:array[0..10000] of longint;
procedure gb(low,high:longint);
var
q,w,e,mid,k:longint;
begin
if low=high then exit;
mid:=(low+high) div 2;
gb(low,mid);
gb(mid+1,high);
q:=low;
w:=mid+1;
e:=low;
while (q<=mid) and (w<=high) do
if a[q]<a[w] then
begin
a2[e]:=a[q];
inc(e);
inc(q);
end else
begin
a2[e]:=a[w];
inc(e);
inc(w);
end;
if q<=mid then
while q<=mid do
begin
a2[e]:=a[q];
inc(e);
inc(q);
end else
while w<=high do
begin
a2[e]:=a[w];
inc(e);
inc(w);
end;
for k:=low to high do
a[k]:=a2[k];
end;
begin
readln(n);
for i:=1 to n do
read(a[i]);
gb(1,n);
for i:=1 to n do
d[1,i]:=a[i];
d[1,0]:=n;
while (d[1,0]+d[2,0]<>1) do
begin
if d[1,0]=0 then begin
for i:=t2+1 to d[2,0]+t2 do
begin
d[1,i-t2]:=d[2,i];
d[2,i]:=0;
end;
d[1,0]:=d[2,0];
d[2,0]:=0;
t1:=0; t2:=0;
end;
q:=0;
w:=0;
while (q+w<2) and (q<d[1,0]) and (w<d[2,0]) do
if d[1,q+t1+1]<=d[2,w+t2+1] then inc(q) else inc(w);
if q=d[1,0] then if q+w<2 then w:=w+(2-q);
if w=d[2,0] then if q+w<2 then q:=q+(2-w);
d[1,0]:=d[1,0]-q;
h:=0;
for i:=t1+1 to t1+q do
begin
h:=h+d[1,i];
d[1,i]:=0;
end;
for i:=t2+1 to t2+w do
begin
h:=h+d[2,i];
d[2,i]:=0;
end;
ans:=ans+h;
d[2,d[2,0]+t2+1]:=h;
d[2,0]:=d[2,0]+1-w;
t1:=t1+q;
t2:=t2+w;
end;
writeln(ans);
end.
小结:
用这样的方法来解哈夫曼问题,经过测试,时间同为0.20s左右。等等,不是说更快吗?再看一个例子:NOI2015荷马史诗
题目描述
追逐影子的人,自己就是影子 ——荷马
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个字符组成的字符串。
算法改进:
样例就不在给出了,直接分析吧。还是同样的思路,再开一个记录类型,w表示长度,m表示深度,然后同样的方法,第一问很好解决,但是要考虑最小深度,这个算法是否可以解决?可以!(读者自己思考证明)其中有个细节必须注意:
if d[1,q+t1+1].w<=d[2,w+t2+1].w then inc(q) else inc(w);
这个等于号相当的重要,在权值相同时,必须优先取第一队列的,因为题目要求在总和最小情况下深度最小,而深度的转移是合并时各个元素的深度的最大值加一,在权值相等情况下,第一队列深度恒小于第二队列深度(有趣的问题当然要自己证明),若不加“=”,只有70分到手(测试过的),一半的点只能拿第一问的分数,所以很不理想,“=”是必不可少的。
时间复杂度为$O(NlogN+2*(N div K)*K)$,在这时,这个算法的优势完全暴露了出来,总用时0.5s,而同等Pascal用堆写2.1S左右,速度为堆的4倍!!!而C++最快的为0.4s左右,基本持平(虽然代码长了点)。因为前面有过框架,就不在就代码给出分析了,认真阅读一下补充数的阶段,进行处理的这几部分,体会和上一题的不同之处。附上代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
program zht;
type
z=record
w:int64;
m:longint;
end;
var
i,n,p,m,t1,t2,q,w,k:longint;
a,a2:array[0..110000] of int64;
ans1,ans2,h:int64;
d:array[1..2,0..110000] of z;
procedure gb(low,high:longint);
var
q,w,e,mid,k:longint;
begin
if low=high then exit;
mid:=(low+high) div 2;
gb(low,mid);
gb(mid+1,high);
q:=low;
w:=mid+1;
e:=low;
while (q<=mid) and (w<=high) do
if a[q]<a[w] then
begin
a2[e]:=a[q];
inc(e);
inc(q);
end else
begin
a2[e]:=a[w];
inc(e);
inc(w);
end;
if q<=mid then
while q<=mid do
begin
a2[e]:=a[q];
inc(e);
inc(q);
end else
while w<=high do
begin
a2[e]:=a[w];
inc(e);
inc(w);
end;
for k:=low to high do
a[k]:=a2[k];
end;
begin
assign(input,'epic.in');
assign(output,'epic.out');
reset(input);
rewrite(output);
readln(n,k);
for i:=1 to n do
read(a[i]);
p:=n;
while (p-1) mod (k-1)<>0 do
inc(p);
gb(1,p);
for i:=1 to p do
begin
d[1,i].w:=a[i];
if a[i]=0 then d[1,i].m:=-maxlongint;
end;
d[1,0].w:=p;
while d[1,0].w+d[2,0].w<>1 do
begin
if d[1,0].w=0 then begin
for i:=t2+1 to t2+d[2,0].w do
begin
d[1,i-t2].w:=d[2,i].w;
d[1,i-t2].m:=d[2,i].m;
d[2,i].w:=0; d[2,i].m:=0;
end;
d[1,0].w:=d[2,0].w;
d[2,0].w:=0;
t1:=0; t2:=0;
end; // zhuan yi
q:=0;
w:=0;
while (q+w<>k) and (q<d[1,0].w) and (w<d[2,0].w) do
if d[1,q+t1+1].w<=d[2,w+t2+1].w then inc(q) else inc(w);
if q+w<>k then if q=d[1,0].w then w:=w+(k-q-w) else q:=q+(k-w-q); // qu shu
d[1,0].w:=d[1,0].w-q;
m:=0;
d[2,d[2,0].w+t2+1].w:=ans1;
for i:=t1+1 to t1+q do
begin
ans1:=ans1+d[1,i].w;
d[1,i].w:=0;
if d[1,i].m>m then m:=d[1,i].m;
d[1,i].m:=0;
end;
for i:=t2+1 to t2+w do
begin
ans1:=ans1+d[2,i].w;
d[2,i].w:=0;
if d[2,i].m>m then m:=d[2,i].m;
d[2,i].m:=0;
end;
d[2,d[2,0].w+t2+1].w:=ans1-d[2,d[2,0].w+t2+1].w;
d[2,d[2,0].w+t2+1].m:=m+1;
d[2,0].w:=d[2,0].w-w+1;
t1:=t1+q;
t2:=t2+w;
end;
writeln(ans1);
writeln(m+1);
close(input);
close(output);
end.
总结
根据以上的分析,这种算法大大提高了运算速度,在数据比较大时,远远超过了堆。所以,这是一个优秀的解决哈夫曼树问题的算法。(算法为本人原创,严禁抄袭)