• Evanyou Blog 彩带


      题目传送门

    数字序列

    题目描述

    给定一个整数序列 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
    ",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;
    } 

     

  • 相关阅读:
    html标签笔记
    C语言中的函数基础
    数组
    循环控制结构程序(goto、while、dowhile、 for)
    if语句+switch
    分支程序设计
    字符数据的输入与输出
    各类数值型数据之间的混合运算
    c语言概述及如何上机运行c程序
    [记录] Ubuntu 配置Apache虚拟站点
  • 原文地址:https://www.cnblogs.com/cytus/p/9210989.html
Copyright © 2020-2023  润新知