数字序列
题目描述
给定一个整数序列 a1,a2,⋅⋅⋅,an ,求出一个递增序列 b1<b2<⋅⋅⋅<bn ,使得序列 ai 和 bi 的各项之差的绝对值之和 ∣a1−b1∣+∣a2−b2∣+⋅⋅⋅+∣an−bn∣ 最小。
输入输出格式
输入格式:
第一行为数字 n (1≤n≤10^6) ,接下来一行共有 n 个数字,表示序列 a_i (0≤a_i≤2×10^9) 。
输出格式:
第一行输出最小的绝对值之和。
第二行输出序列 bi ,若有多种方案,只需输出其中一种。
输入输出样例
说明
【数据范围】
40%的数据 n≤5000
60%的数据 n≤300000
100%的数据 n≤1000000 , 0≤a_i≤2×10^9
题目来源: Baltic OI 2004 Day1, Sequence
分析:
不得不说,一道左偏树神仙题。一开始看到这道题,基本上想到的都是数学方法,或者是想到线段树之类的数据结构。后面才知道是左偏树,而且是个论文题,思路实在巧妙。
以下直接引用自黄源河的论文,这里蒟蒻就只放代码:
我们先来看看两个最特殊的情况:
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+1],我们需要将最后两个区间合并,并找出新区间的 最优解(也就是序列 a 中,下标在这个新区间内的各项的中位数)。重复这个合 并过程,直至 w[1]≤w[2]≤…≤w[m]时结束,然后继续处理下一个数。
这个算法的正确性前面已经论证过了,现在我们需要考虑一下数据结构的选 取。算法中涉及到以下两种操作:合并两个有序集以及查询某个有序集内的中位 数。能较高效地支持这两种操作的数据结构有不少,一个比较明显的例子是二叉 检索树(BST),它的询问操作复杂度是O(logn),但合并操作不甚理想,采用启发 式合并,总时间复杂度为O(nlog2 n)。
有没有更好的选择呢?通过进一步分析,我们发现,只有当某一区间内的中 位数比后一区间内的中位数大时,合并操作才会发生,也就是说,任一区间与后 面的区间合并后,该区间内的中位数不会变大。于是我们可以用最大堆来维护每 个区间内的中位数,当堆中的元素大于该区间内元素的一半时,删除堆顶元素, 这样堆中的元素始终为区间内较小的一半元素,堆顶元素即为该区间内的中位 数。考虑到我们必须高效地完成合并操作,左偏树是一个理想的选择* 。左偏树 的询问操作时间复杂度为O(1),删除和合并操作时间复杂度都是O(logn),而询 问操作和合并操作少于n次,删除操作不超过n/2 次(因为删除操作只会在合并两 个元素个数为奇数的堆时发生),因此用左偏树实现,可以把算法的时间复杂度 降为O(nlogn)。
Code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+;
int n,top;ll b[N],ans;
struct Node{
int l,r,top,size;ll val;
}stac[N];
struct Leftist{
int fa[N],ch[N][],dis[N];
ll val[N];
int merge(int x,int y)
{
if(x==||y==)return x+y;
if(val[x]<val[y])swap(x,y);
ch[x][]=merge(ch[x][],y);
fa[ch[x][]]=x;
if(dis[ch[x][]]>dis[ch[x][]])
swap(ch[x][],ch[x][]);
dis[x]=dis[ch[x][]]+;
return x;
}
int del(int x)
{
fa[ch[x][]]=fa[ch[x][]]=;
int ret=merge(ch[x][],ch[x][]);
ch[x][]=ch[x][]=;
return ret;
}
}T;
inline ll read()
{
char ch=getchar();ll num=;bool flag=false;
while(ch<''||ch>''){if(ch=='-')flag=true;ch=getchar();}
while(ch>=''&&ch<=''){num=num*+ch-'';ch=getchar();}
return flag?-num:num;
}
void work()
{
stac[++top]=(Node){,,,,T.val[]};
for(int i=;i<=n;i++){
int l=stac[top].r+;
stac[++top]=(Node){l,i,i,,T.val[i]};
while(top!=&&stac[top].val<stac[top-].val){
--top;
stac[top].top=T.merge(stac[top].top,stac[top+].top);
stac[top].size=stac[top].size+stac[top+].size;
stac[top].r=stac[top+].r;
while(stac[top].size>(stac[top].r-stac[top].l+)/){
--stac[top].size;stac[top].top=T.del(stac[top].top);}
stac[top].val=T.val[stac[top].top];
}
}
int cnt=;
for(int i=;i<=n;i++){
while(i>stac[cnt].r)cnt++;
ans+=abs(T.val[i]-stac[cnt].val);
b[i]=stac[cnt].val+i;}
printf("%lld\n",ans);
for(int i=;i<=n;i++)
printf("%lld ",b[i]);
}
int main()
{
n=read();
for(int i=;i<=n;i++){
T.val[i]=read();
T.val[i]-=i;}
work();return ;
}