玩具装箱toy

好久都没写过blog了。。。虽然本来也写的不怎么样。。。
不过今天看那个专题看了几乎一整天。。。最后才A掉一个题,而且现在也不好说到底有没有真正掌握。所以我觉得还是写一下加深印象比较好。

还是直接从题目出发吧。。
题目链接


C - 玩具装箱toy

 P教授要去看奥运,但是他舍不下他的玩具,于是他决定把所有的玩具运到北京。他使用自己的压缩器进行压缩,其可以将任意物品变成一堆,再放到一种特殊的一维容器中。P教授有编号为1…N的N件玩具,第i件玩具经过压缩后变成一维长度为Ci.为了方便整理,P教授要求在一个一维容器中的玩具编号是连续的。同时如果一个一维容器中有多个玩具,那么两件玩具之间要加入一个单位长度的填充物,形式地说如果将第i件玩具到第j个玩具放到一个容器中,那么容器的长度将为 x=j-i+Sigma(Ck) i<=K<=j 制作容器的费用与容器的长度有关,根据教授研究,如果容器长度为x,其制作费用为(X-L)^2.其中L是一个常量。P教授不关心容器的数目,他可以制作出任意长度的容器,甚至超过L。但他希望费用最小.

Input

 第一行输入两个整数N,L.接下来N行输入Ci.1<=N<=50000,1<=L,Ci<=10^7

Output

 输出最小费用

Sample Input

5 4

3

4

2

1

4

Sample Output

1


中文题面很好懂,作为一个dp题来说,普通的dp方程也非常容易找,但是数据范围:N是5e4,用普通dp的话需要O(n²),显然会T

这时候要采用一种名为斜率优化的玄学的东西,就算我看了一整天也还是似懂非懂。

我觉得网上关于斜率优化讲的都不是很清楚(大概是我太菜),(事实是我发现我自己也讲不清楚)

首先从题目出发,至于普通的dp方程,我就直接给出来了

设dp[i]表示前i个玩具所需要的最小消耗,sum[i]表示前i个物品的体积,则有

dp[i]=min(dp[i],dp[j]+(sum[i]sum[j]+ij1L)2) d p [ i ] = m i n ( d p [ i ] , d p [ j ] + ( s u m [ i ] − s u m [ j ] + i − j − 1 − L ) 2 )

显然,通过这个dp方程暴力解决的复杂度为O(n²),对于5e4的数据还是有点吃力的,所以我们要对其做一些优化,即所谓的斜率优化

优化的部分

先看一下原来的dp的核心代码:

dp[0] = cost(0, 0);//cost[i][j]表示合并从i到j的玩具所需要的花费
for(int i = 1; i < n; i++) {
    dp[i] = dp[i-1] + cost(i, i);//初始值
    for(int j = 0; j < i; j++)
        dp[i] = min(dp[i], dp[j] + cost(j+1, i));
}

我们斜率优化主要改变的是内层循环的部分,甚至优化到你看代码都看不出来那是个dp(我菜我菜)。

内层循环本来是要遍历前面可以选择的所有点(O(n)),然后选出“最优”。在斜率优化中,我们采用一个双端队列来存储待选的所有的点,而把不可能的点通过某种玄学方法把它筛出去,这样内层遍历的复杂度将会大大小于O(n),其伪代码为

for(int i = 1; i < n; i++) {
    维护队首();
    处理要插入的点();
    维护队尾();
    插入点();
}

在处理第i个点时,经过前面i-1次循环已经将0~i-1中所有的点都入队了,并且其中的无效的点已经被删除出去不用管

这样内层循环每次只需要遍历少量的点,可以大大减小复杂度。

维护队首

首先要明确的是删除队首的点是为了在插入下一个结点之前保证结果的最优,怎么来保证最优呢?就像算法的名字提到的一样,用到了斜率,而且在这题中,是随着遍历的深入,斜率越大,结果最优

接下来给出证明:

由于循环是有序的,且入队的方向是有序的,再加上输入是有序的,所以队列中的点也是有序的

对于队列中有序的点(什么点?可以先往下看),不妨设两个点的下标为j,k,且满足k>j,此时我们讨论当什么情况下k会优于j,从而将j出队

这时候就要引出我们的“斜率”

斜率是什么

为了表示更方便,我们将dp方程修改一下形式:

dp[i]=dp[j]+(sum[i]sum[j]+ij1L) d p [ i ] = d p [ j ] + ( s u m [ i ] − s u m [ j ] + i − j − 1 − L )

令:

s[i]=sum[i]+i; s [ i ] = s u m [ i ] + i ;

C=1+L; C = 1 + L ;

则原来的方程变为

dp[i]=dp[j]+(s[i]s[j]C)2 d p [ i ] = d p [ j ] + ( s [ i ] − s [ j ] − C ) 2

我们先找一个方法能判定两个点的优劣

设:对于某个特定的i(即将i视为常数),i>k>j且k优于j,因为题目中是求最小,所以由dp方程知:

dp[k]+(s[i]s[k]C)2dp[j]+(s[i]s[j]C)2 d p [ k ] + ( s [ i ] − s [ k ] − C ) 2 ≤ d p [ j ] + ( s [ i ] − s [ j ] − C ) 2

将括号打开,变为

dp[k]+s[i]22(s[k]+C)s[i]+(s[k]+C)2dp[j]+s[i]22(s[j]+C)s[i]+(s[j]+C)2 d p [ k ] + s [ i ] 2 − 2 ∗ ( s [ k ] + C ) ∗ s [ i ] + ( s [ k ] + C ) 2 ≤ d p [ j ] + s [ i ] 2 − 2 ∗ ( s [ j ] + C ) ∗ s [ i ] + ( s [ j ] + C ) 2

等式两边约掉相同的且含i的项:s[i] - 2*C*s[i],并把含i的项移到不等式一遍,得到:

2s[i](s[k]s[j])(dp[k]+(s[k]+C)2)(dp[j]+(s[j]+C)2) 2 s [ i ] ∗ ( s [ k ] − s [ j ] ) ≥ ( d p [ k ] + ( s [ k ] + C ) 2 ) − ( d p [ j ] + ( s [ j ] + C ) 2 )

显然 s[i]=sum[i]+i s [ i ] = s u m [ i ] + i 是单调递增的,又已经假设 k>j k > j ,所以将 i i 分离,变为

s[i](dp[k]+(s[k]+C)2)(dp[j]+(s[j]+C)2)2(s[k]s[j])

化简到这里就比较明显了,如果我们设 Yi=dp[i]+(s[i]+C)2 Y i = d p [ i ] + ( s [ i ] + C ) 2 ,且 Xi=2s[i] X i = 2 ∗ s [ i ] ,那么上式变为:

s[i]YkYjXkXj s [ i ] ≥ Y k − Y j X k − X j

这样看我们就很熟悉了,不等式的右边就是我们常见的斜率的形式,即以k为下标的点和以j为下标的点对应的斜率。

斜率怎么用?

这时候我们就要回到我们的假设中来,上述推论中,我们的假设是:对于某个特定的i(即将i视为常数),i>k>j且k优于j,通过这个假设得到了不等式

s[i]YkYjXkXj s [ i ] ≥ Y k − Y j X k − X j

这也就是说,当这个不等式满足时,对于i来说,dp[i]的取值将会在k处取到,因为其所对的值更小,所以k一定优于j,而且s[i]是递增的,此时可以将j出队

核心代码

根据上述公式求出两点的斜率:

double scope(ll k, ll j) {
    return (dp[k] + square(sum[k]+L+1)
          - dp[j] - square(sum[j]+L+1))
          / (2.0*(sum[k] - sum[j]));
}

维护队首

while(!que.isEmpty() && scope(que.get(0), que.get(1)) <= sum[i]) que.popHead();

处理要插入的点

这一步主要是在队首得到最优情况后,处理即将插入的点的信息,这没什么特别需要注意的

long long t = que.get(0);
dp[i] = dp[t] + square(s[i] - s[t] - C);

维护队尾

由上面的结论我们知道:当方程

s[i]YkYjXkXj s [ i ] ≥ Y k − Y j X k − X j

满足时,k要优于j,那么如果有j,k,l三个变量,如果j < k < l,且scope(j, k) < scope(k, l),那么一定可以排除k

因为若scope(j, k) < s[i],那么j已经优于k了,可以排除k

若scope(j, k) >= s[i],那么一定有scope(k, l) > scope(j, k) >= s[i],则scope(l, k) < s[i],故l优于k,可以排除k

这和我们维护下凸包的方法类似,即插入一个点之前比较与前一个点的斜率,若之前的点无法维护下凸包性质,则将其出队

在这里,我们加入一个点之前就可以和队尾两个点的斜率进行比较

while(!que.isEmpty() &&
    scope(que.get(que.size()-1), i) <
    scope(que.get(que.size()-1), que.get(que.size())))
        que.popTail();

插入点

没什么好说的。。

que.pushHead(i);

汇总

for(ll i = 1; i <= n; i++) {
    //维护队首
    while(!que.isEmpty() && scope(que.get(0), que.get(1)) <= sum[i]) que.popHead();
    //要插入的点
    ll t = que.get(0);
    dp[i] = dp[t] + square(sum[i] - sum[t] - 1 - L);
    //维护队尾
    while(!que.isEmpty() &&
        scope(que.get(que.size()-1), i) < scope(que.get(que.size()-1), que.get(que.size())))
            que.popTail();
    //插入
    que.pushHead(i);
}

emmmm上面的代码已经可以看出来了。。。我自己写了个queue。。。所以很丑很丑很丑而且又臭又长。。。所以最后的代码emmmm随意吧。。。

#include<iostream>
#include<cstring>
#include<algorithm>
#define maxn 50005
using namespace std;
typedef long long ll;
struct doubleQueue {
    ll l, r;
    ll Q[maxn];
    doubleQueue(ll l = 0, ll r = 0):l(l),r(r) {}
    bool isEmpty() { return !(r > l); }
    void pushHead(ll i) { Q[++r] = i; }
    ll popHead() { return Q[l++]; }
    ll popTail() { return Q[r--]; }
    ll get(ll idx) { return Q[l+idx]; }
    ll size() { return r-l; }
} que;
ll n, L;
ll sum[maxn];
ll dp[maxn];
ll square(ll a) { return a*a; }
double scope(ll k, ll j) {
    return (dp[k] + square(sum[k]+L+1) - dp[j] - square(sum[j]+L+1)) / (2.0*(sum[k] - sum[j]));
}
int main() {
    while(cin>>n>>L) {
        sum[0] = 0;
        for(ll i = 1; i <= n; i++) {
            ll tmp; cin>>tmp;
            sum[i] = sum[i-1] + tmp;
        }
        for(ll i = 1; i <= n; i++) sum[i] += i;

        for(ll i = 1; i <= n; i++) {
            while(!que.isEmpty() && scope(que.get(0), que.get(1)) <= sum[i]) que.popHead();
            ll t = que.get(0);
            dp[i] = dp[t] + square(sum[i] - sum[t] - 1 - L);
            while(!que.isEmpty() &&
                scope(que.get(que.size()-1), i) < scope(que.get(que.size()-1), que.get(que.size())))
                    que.popTail();
            que.pushHead(i);
        }

        cout<<dp[n]<<endl;
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值