黄源河《左偏树的应用》——数字序列(Baltic 2004)

这道题哪里都找不到。

[问题描述]

给定一个整数序列a1, a2, … , an,求一个不下降序列b1 ≤ b2 ≤ … ≤ bn,使得数列{ai}和{bi}的各项之差的绝对值之和 |a1 - b1| + |a2 - b2| + … + |an - bn| 最小。

[数据规模] 1 ≤ n ≤ 106, 0 ≤ ai ≤ 2*109

    这道题很有趣,值得一做。

    我们对于这种题目,要有有效的思维方式。

    ①:考虑最终的答案数列B,它能够看成是很多段相同的数段连接而成的,构成一个不下降的序列。(比如说:111 2 33333 4444 5)

    ②:猜想如何获得B序列,根据B序列的性质(①),我们猜测某两个连续的区间最优的答案(规定这两段序列分别是同一个值)可以快速合并。下面来"伪"证一下猜想。两段在位置上是相邻的,设左半边的最优值为U,右半边为V,若U≤V,则两段答案可以共存。不然,则需要继续探讨:(这时两段的最优解是不能共存的,因为这不能构成一个不下降的B数列),因此需要找到一个新的答案,为B[1~n(并上)n+1~m](其中原B[1~n]为U,原B[n+1~m]为V),先挖掘一下性质:1)B[n]<U,2)B[n+1]>V。这两点的正确性是显然的。假设我们现在已经知道B[n]应该是哪个数,那么B[n-1]我是希望尽量大的(因为B[n-1]≤B[n]<U,U是左半段的最优解,贪心的策略就是让B[n-1]的值尽量靠近U,所以B[n-1]=B[n]才是最理想的,继续考虑B[n-2]的值,B[n-2]=B[n-1],B[n-3]=B[n-2]……,最终得出左半段是一个值,同理右半段也是一个值,那左右是否就是同一个值呢?(不是的话这题还能怎么做呢?)假设新的答案为U1和V1,U1≤V1,若U1<V1,则贪心的,我让U1等于V1,则答案会更优,所以U1必定等于V1。太棒了,我现在用一堆弱弱的证明解决了大部分问题,还要考虑的就是U1的求法。

    ③:由于是同一个值,是否直接用中位数就好了?让人欣喜若狂的是,这是显然的(我都懒得证了)。为了编程方便,用排序后的A数组下标为floor(M/2)也是对的。

    ④:可以得到这样一个算法:

        from 1 to m

        新建一个答案区间i~i,值为A[i]

        if(A[i]小于它左边区间的答案)

            合并两个区间

            获取"中位数"作为新区间的答案

        endif

        else    continue

    ⑤:如何合并?考虑平衡树?太麻烦了,考虑用主席树或可并堆吧。主席树就是求区间第K大,几乎模板;而可并堆的做法复杂一些

    好感人,至此这道题应该就没有什么疑点了吧。

    我觉得做这道题要靠运气,很多地方要先猜到再证明,没有绝对的实力是很痛苦的,希望在不久的将来我能有绝对的实力。

    这是黄源河大牛的题解。(简直没得比)

[初步分析]

我们先来看看两个最特殊的情况:

  1. a[1]≤a[2]≤…≤a[n],在这种情况下,显然最优解为b[i]=a[i];
  2. a[1]≥a[2]≥…≥a[n],这时,最优解为b[i]=x,其中x是数列a的中位数。

于是我们可以初步建立起这样一个思路:

把1…n划分成m个区间:[q[1],q[2]-1],[q[2],q[3]-1],……,[q[m],q[m+1]-1]。每个区间对应一个解,b[q[i]] = b[q[i]+1] = … = b[q[i+1]-1] = w[i],其中w[i] 为a[q[i]], a[q[i]+1], ... , a[q[i+1]-1] 的中位数。

显然,在上面第一种情况下m=n,q[i]=i;在第二种情况下m=1,q[1]=1。

这样的想法究竟对不对呢?应该怎样实现?

若某序列前半部分a[1], a[2], … , a[n] 的最优解为 (u,u,…,u),后半部分a[n+1], a[n+2], ... , a[m] 的最优解为 (v,v,…,v),那么整个序列的最优解是什么呢?若u≤v,显然整个序列的最优解为 (u,u,…,u,v,v,…,v)。否则,设整个序列的最优解为 (b[1],b[2],…,b[m]),则显然b[n]≤u(否则我们把前半部分的解 (b[1],b[2],…,b[n]) 改为 (u,u,…,u),由题设知整个序列的解不会变坏),同理b[n+1]≥v。接下来,我们将看到下面这个事实:

对于任意一个序列a[1],a[2],…,a[n],如果最优解为 (u,u,…,u),那么在满足u≤u′≤b[1] 或b[n]≤u′≤u的情况下,(b[1],b[2],…,b[n]) 不会比 (u′,u′,…,u′) 更优。

我们用归纳法证明u≤u′≤b[1] 的情况,b[n]≤u′≤u的情况可以类似证明。

当n=1时,u=a[1],命题显然成立。

当n>1时,假设对于任意长度小于n的序列命题都成立,现在证明对于长度为n的序列命题也成立。首先把 (b[1], b[2], … b[n]) 改为 (b[1], b[1], … b[1]),这一改动将不会导致解变坏,因为如果解变坏了,由归纳假设可知a[2],a[3],…,a[n]的中位数w>u,这样的话,最优解就应该为(u,u,…,u,w,w,…,w),矛盾。然后我们再把(b[1],b[1],…,b[1])改为 (u′,u′,…,u′),由于| a[1]- x | + | a[2]- x | + … + | a[n]- x | 的几何意义为数轴上点x到点a[1], a[2], … a[n]的距离之和,且u≤u′≤b[1],显然点u′到各点的距离之和不会比点b[1]到各点的距离之和大,也就是说,(b[1],b[1],…,b[n]) 不会比 (v,v,…,v) 更优。(证毕)

再回到之前的论述,由于b[n]≤u,作为上述事实的结论,我们可以得知,将(b[1],b[2],…,b[n])改为(b[n],b[n],…,b[n]),再将(b[n+1],b[n+2],…,b[m])改为(b[n+1],b[n+1],…,b[n+1]),并不会使解变坏。也就是说,整个序列的最优解为(b[n],b[n],…,b[n],b[n+1],b[n+1],…,b[n+1])。再考虑一下该解的几何意义,设整个序列的中位数为w,则显然令b[n]=b[n+1]=w将得到整个序列的最优解,即最优解为 (w,w,…,w)。

分析到这里,我们一开始的想法已经有了理论依据,算法也不难构思了。

[算法描述]

延续我们一开始的思路,假设我们已经找到前k个数a[1], a[2], … , a[k] (k<n) 的最优解,得到m个区间组成的队列,对应的解为 (w[1],w[2],…,w[m]),现在要加入a[k+1],并求出前k+1个数的最优解。首先我们把a[k+1]作为一个新区间直接加入队尾,令w[m+1]=a[k+1],然后不断检查队尾两个区间的解w[m]和w[m+1],如果w[m]>w[m+1],我们需要将最后两个区间合并,并找出新区间的最优解(也就是序列a中,下标在这个新区间内的各项的中位数)。重复这个合并过程,直至w[1]≤w[2]≤…≤w[m]时结束,然后继续处理下一个数。

这个算法的正确性前面已经论证过了,现在我们需要考虑一下数据结构的选取。算法中涉及到以下两种操作:合并两个有序集以及查询某个有序集内的中位数。能较高效地支持这两种操作的数据结构有不少,一个比较明显的例子是二叉检索树(BST),它的询问操作复杂度是O(logn),但合并操作不甚理想,采用启发式合并,总时间复杂度为O(nlog2n)。

有没有更好的选择呢?通过进一步分析,我们发现,只有当某一区间内的中位数比后一区间内的中位数大时,合并操作才会发生,也就是说,任一区间与后面的区间合并后,该区间内的中位数不会变大。于是我们可以用最大堆来维护每个区间内的中位数,当堆中的元素大于该区间内元素的一半时,删除堆顶元素,这样堆中的元素始终为区间内较小的一半元素,堆顶元素即为该区间内的中位数。考虑到我们必须高效地完成合并操作,左偏树是一个理想的选择。左偏树的询问操作时间复杂度为O(1),删除和合并操作时间复杂度都是O(logn),而询问操作和合并操作少于n次,删除操作不超过n/2次(因为删除操作只会在合并两个元素个数为奇数的堆时发生),因此用左偏树实现,可以把算法的时间复杂度降为O(nlogn)。

[小结]

这道题的解题过程对我们颇有启示。在应用左偏树解题时,我们往往会觉得题目无从下手,甚至与左偏树毫无关系,但只要我们对题目深入分析,加以适当的转化,问题终究会迎刃而解。这需要我们具有敏捷的思维以及良好的题感。

用左偏树解本题,相比较于前面BST的解法,时间复杂度和编程复杂度更低,这使我们不得不感叹于左偏树的神奇威力。这不是说左偏树就一定是最好的解法,就本题来说,解法有很多种,光是可并堆的解法,就可以用多种数据结构来实现,但左偏树相对于它们,还是有一定的优势的,这将在下一章详细讨论。

 #include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
const int maxn=;
int B[maxn];
int q[maxn],l[maxn],r[maxn],num[maxn],dep[maxn],tot;
int ch[maxn][],key[maxn],n;
int Merge(int x,int y){
if(!x||!y)return x|y;
if(key[x]<key[y])swap(x,y);
ch[x][]=Merge(ch[x][],y);
if(dep[ch[x][]]>dep[ch[x][]])
swap(ch[x][],ch[x][]);
dep[x]=dep[ch[x][]]+;
return x;
} void Delete(int i){
q[i]=Merge(ch[q[i]][],ch[q[i]][]);
} int main(){
dep[]=-;
scanf("%d",&n);
for(int i=;i<=n;i++)
scanf("%d",&key[i]);
for(int i=;i<=n;i++){
q[++tot]=i;l[tot]=r[tot]=i;num[tot]=;
while(tot!=&&key[q[tot]]<key[q[tot-]]){
q[tot-]=Merge(q[tot],q[tot-]);
num[tot-]+=num[tot];
r[tot-]=r[tot];tot--;
int sum=(r[tot]-l[tot]+)/;
while(num[tot]>sum){
Delete(tot);
num[tot]-=;
}
}
} for(int i=;i<=tot;i++)
for(int j=l[i];j<=r[i];j++)
B[j]=key[q[i]]; int ans=;
for(int i=;i<=n;i++)
ans+=abs(key[i]-B[i]);
printf("%d\n",ans);
return ;
}
上一篇:2017.4.7 Sprng MVC工作流程描述图


下一篇:Django中间件的使用