以后写文章前先Orz Jerrydeng.
之前讲过左偏树在dispatching中的应用,但是那还不够经典.左偏树最经典的应用,就是黄源河大牛05年论文中的那道例题.
题目是这样的:
给一个序列a,要求你构造一个长度与a相同的非降序列b,使sigma(abs(ai-bi))最小
黄源河对这个问题进行了细致的讲解,但是里面的证明略显学术了,一般的人(比如我)看了一半就不想看了.个人觉得与其看他的论文,还不如看他的ppt.
总之,这道题的做法是这样的:维护一些连续的区间,每次加进第i个元素,将他作为一个独立的区间.每当最后一个区间与倒数第二个区间相冲突,就把两个区间合并,并把这两个区间中的b值都给为他们的中位数.
先说明为什么要改成中位数:首先,由于最后两个区间有冲突,所以这两个区间一定要全部改成同一个数(设为k),修改这两段区间的代价就是sigma(abs(k-ai)),想象一个数轴,在每个ai上放一个物品,abs(k-ai)就是k与ai的距离.由常识我们可以知道,不会有方案比k为中位数时更优(证明自己yy).所以,如果两个区间相冲突,应该将区间中的数都改为中位数.
这样,算法的大致流程就出来了:维护一个栈,设一个指针i,每次指针往后扫时将区间[i,i]入栈,然后检查栈顶元素是否跟他下面的元素冲突了,如果是就合并最上面两个区间,直到只剩一个元素或没有冲突.
这个算法需要一个数据结构支持以下操作:
1.查找一个区间的中位数.
2.合并两个区间
本来如果只是这样还不能用左偏树,只能用平衡树,但是,这道题有一个性质:某一个区间跟后面一个区间合并后中位数不会变大(想想为什么),于是,左偏树做法就呼之欲出了:对每个区间,维护一个大根堆,如果堆中元素超过区间长度的一半就删掉堆顶元素,这样最后的堆顶元素就是该区间的中位数.这个算法大致就是这样,顺便再Orz一下Jerrydeng.
Code:
program road;
type
int=int64;
var
i,j:longint;
k,m,p,n:int;
ll,rr,a,l,r,rt,s,dis,b:array[-1..100000]of int;{ll和rr是区间端点;rt是这个区间的root;l,r,dis就是左偏树里要用的三个数组;s是子树中的节点数}
ans:int;
procedure swap(var x,y:int);
var t:int;
begin
t:=x;x:=y;y:=t;
end;
function union(x,y:int):int;
begin
if(x=-1)or(y=-1)then exit(x+y+1);
if a[x]<a[y] then swap(x,y);
r[x]:=union(y,r[x]);
if dis[l[x]]<dis[r[x]]then swap(l[x],r[x]);
dis[x]:=dis[r[x]]+1;
s[x]:=s[l[x]]+s[r[x]]+1;
exit(x);
end;
procedure merge(x,y:int);
var m,sum:int;
begin
rt[x]:=union(rt[x],rt[y]);
ll[x]:=ll[x];rr[x]:=rr[y];
m:=rr[x]-ll[x]+1;
sum:=(m>>1)+(m and 1);
while s[rt[x]]>sum do rt[x]:=union(l[rt[x]],r[rt[x]]);
end;
begin
assign(input,'road.in');reset(input);
assign(output,'road.out');rewrite(output);
read(n);
s[-1]:=0;dis[-1]:=0;
p:=0;
for i:=1 to n do begin
read(a[i]);
inc(p);
l[i]:=-1;r[i]:=-1;
s[i]:=1;rt[p]:=i;
dis[i]:=dis[r[i]]+1;
ll[p]:=i;rr[p]:=i;
while(p<>1)and(a[rt[p]]<a[rt[p-1]])do begin
merge(p-1,p);
dec(p);
end;
end;
for i:=1 to p do
for j:=ll[i]to rr[i] do b[j]:=a[rt[i]];
ans:=0;
for i:=1 to n do inc(ans,abs(b[i]-a[i]));
write(ans);
close(input);close(output);
end.
以上的证明不是很严谨,有兴趣的同学最好还是看一下黄源河的论文.
BY QW
转载请注明出处