洛谷P4331 [BOI2004] Sequence 数字序列 [左偏树]

  题目传送门

数字序列

题目描述

给定一个整数序列 a1,a2,,an ,求出一个递增序列 b1<b2<<bn ,使得序列 ai 和 bi 的各项之差的绝对值之和 a1b1+a2b2++anbn∣ 最小。

输入输出格式

输入格式:

 

第一行为数字 n (1≤n≤10^6) ,接下来一行共有 n 个数字,表示序列 a_i (0≤a_i≤2×10^9) 。

 

输出格式:

 

第一行输出最小的绝对值之和。

第二行输出序列 bi ,若有多种方案,只需输出其中一种。

 

输入输出样例

输入样例#1: 复制
5
2 5 46 12 1
输出样例#1: 复制
47
2 5 11 12 13

说明

【数据范围】

40%的数据 n≤5000

60%的数据 n≤300000

100%的数据 n≤1000000 , 0≤a_i≤2×10^9

题目来源: Baltic O200Day1, 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+7; 
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][2],dis[N];
    ll val[N];
    int merge(int x,int y)
    {
        if(x==0||y==0)return x+y;
        if(val[x]<val[y])swap(x,y);
        ch[x][1]=merge(ch[x][1],y);
        fa[ch[x][1]]=x;
        if(dis[ch[x][1]]>dis[ch[x][0]])
            swap(ch[x][0],ch[x][1]);
        dis[x]=dis[ch[x][1]]+1;
        return x;
    }
    int del(int x)
    {
        fa[ch[x][0]]=fa[ch[x][1]]=0;
        int ret=merge(ch[x][0],ch[x][1]);
        ch[x][0]=ch[x][1]=0;
        return ret;
    }
}T;
inline ll read()
{
    char ch=getchar();ll num=0;bool flag=false;
    while(ch<'0'||ch>'9'){if(ch=='-')flag=true;ch=getchar();}
    while(ch>='0'&&ch<='9'){num=num*10+ch-'0';ch=getchar();}
    return flag?-num:num;
}
void work()
{
    stac[++top]=(Node){1,1,1,1,T.val[1]};
    for(int i=2;i<=n;i++){
        int l=stac[top].r+1;
        stac[++top]=(Node){l,i,i,1,T.val[i]};
        while(top!=1&&stac[top].val<stac[top-1].val){
            --top;
            stac[top].top=T.merge(stac[top].top,stac[top+1].top);
            stac[top].size=stac[top].size+stac[top+1].size;
            stac[top].r=stac[top+1].r;
            while(stac[top].size>(stac[top].r-stac[top].l+2)/2){
                --stac[top].size;stac[top].top=T.del(stac[top].top);}
            stac[top].val=T.val[stac[top].top];
        }
    }
    int cnt=1;
    for(int i=1;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=1;i<=n;i++)
    printf("%lld ",b[i]);
}
int main()
{
    n=read();
    for(int i=1;i<=n;i++){
        T.val[i]=read();
        T.val[i]-=i;}
    work();return 0;
} 

 

 

 

 

转载于:https://www.cnblogs.com/cytus/p/9210989.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值