【hihoCoder1529】不上升序列(DP+斜率+堆)

题目描述

给定一个长度为 n ( 1n500000 ) 的非负整数序列 a[1..n]。
你每次可以花费 1 的代价给某个 a[i] ( 0<a[i]109 ) 加1或者减1。
求最少需要多少代价能将这个序列变成一个不上升序列。

题解

首先想到的是最朴素的DP:

dp[i][j]表示前i个数排成不上升序列,最小的数为j,所需的最小代价;

dp[1][j]=|j-a[1]|
枚举k,(k>=a[i])
    dp[i+1][j]=min(dp[i][k])+|j-a[i]|;

令函数 fi(x) 表示dp[i][x],则 fi(x) 一定是这种形状:(下凸包)
fi(x)的图像
由于每次转移时,总是取大于等于某个值的区域里的最小值,所以左半边递减的图像没有任何用处,直接忽略掉:
fi(x)的图像2
此时, fi(x) 即为递增函数,那么 min(fi(k))=fi(a[i]) | (k>=a[i])

还可以发现,这个函数的斜率一定是整数,当x增加1时,相当于数组a至少1个的元素需要更改值,花费自然也是整数,所以斜率也就为整数。
仔细观察,还可以知道,斜率一定是不下降的:当x每增加1,数组a中需要更改的元素个数不可能比以前少,所以增加的代价一定比x小的时候多,斜率也就不下降了。

继续研究转移:
令函数 gi(x)=|xa[i]| ,那么函数 fi+1(x)=fi(x)+gi(x)
函数 gi(x) 图像:
gi(x)图像

p=fi(x) 最小时的x

如果 a[i]p ,像这样,则合成后,相当于 fi(x) 的所有斜率+1,并添加 a[i] p 的一条斜率为1的线段:
合成fi+1(x)(1)

如果pa[i],像这样,则合成后,相当于 fi(x) 在a[i]左边的线段所有斜率-1,在a[i]右边的所有线段斜率+1.
合成fi+1(x)(2)
同样,图上 fi+1(x) 斜率为0的一段我们可以扔掉,注意到此时 fi+1(x) 最小值相比于 fi(x) 增加了 (a[i]p)

实现

实现时,使用一个优先队列(堆)来存储当前 fi 上的斜率变化的折点位置,折点位置在堆中的排名表示这个折点后的线段的斜率。
用ans表示此时 fi 最小值,即为答案。

每添加一个新的a[i],获取p ( p=fi(x) 最小时的x),p=堆顶

如果 a[i]p ,则需要将堆中所有折点的斜率+1,并添加a[i]折点。
只需将a[i]插入堆中即可

如果 pa[i] ,则将堆顶弹出扔掉(因为这个点斜率-1为0,不再需要考虑),ans加上 (a[i]p) ,然后插入2个a[i],因为后面的斜率会比之前的大2(后面+1,前面-1)。

最后从1~n枚举i,最后的ans即为答案

代码

#include<cstdio>
#include<queue>
#include<vector>
using namespace std;

priority_queue< int,vector<int>,greater<int> > Q;

int main()
{
    int n,a;
    long long ans=0LL;
    scanf("%d",&n);
    scanf("%d",&a);
    Q.push(a);
    for(int i=2;i<=n;i++)
    {
        scanf("%d",&a);
        Q.push(a);
        int mn=Q.top();
        if(mn<a)
        {
            Q.pop();
            ans+=1LL*(a-mn);
            Q.push(a);
        }
    }
    printf("%lld\n",ans);
    return 0;
}
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值