BZOJ1010: 玩具装箱 题解

本文详细解析了经典的DP+斜率优化问题,通过数学推导和几何解释,介绍了如何使用单调队列进行状态转移,实现O(n)的时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

经典的dp+斜率优化题
容易想到dp的状态转移方程

dp[i]=min(dp[j]+(sum[i]sum[j]+ij1+l)2)dp[i]=min(dp[j]+(sum[i]−sum[j]+i−j−1+l)2)

其中sum[i]表示1~i的长度总和
我们令s[i]=sum[i]+i,L=l-1,则有
dp[i]=min(dp[j]+(s[i]s[j]+L)2)dp[i]=min(dp[j]+(s[i]−s[j]+L)2)

展开
dp[i]=min(dp[j]+s[i]2+(s[j]+L)22s[i](s[j]+L))dp[i]=min(dp[j]+s[i]2+(s[j]+L)2−2∗s[i]∗(s[j]+L))

我们考虑决策j和k,若决策j优于决策k,则有
dp[j]+s[i]2+(s[j]+L)22s[i](s[j]+L)<dp[k]+s[i]2+(s[k]+L)22s[i](s[k]+L)dp[j]+s[i]2+(s[j]+L)2−2∗s[i]∗(s[j]+L)<dp[k]+s[i]2+(s[k]+L)2−2∗s[i]∗(s[k]+L)

合并同类项
2s[i][(s[k]+L)(s[j]+L)]<(dp[k]+(s[k]+L)2)(dp[j]+(s[j]+L)2)2∗s[i]∗[(s[k]+L)−(s[j]+L)]<(dp[k]+(s[k]+L)2)−(dp[j]+(s[j]+L)2)

把左边的东西除过去
2s[i]<[dp[k]+(s[k]+L)2][dp[j]+(s[j]+L)2](s[k]+L)(s[j]+L)2∗s[i]<[dp[k]+(s[k]+L)2]−[dp[j]+(s[j]+L)2](s[k]+L)−(s[j]+L)

我们发现右边的式子非常像一个求斜率的东西
对于每一个i,我们可以对它生成一个点(s[i]+L,dp[i]+(s[i]+L)2)(s[i]+L,dp[i]+(s[i]+L)2),这样就可以轻松的判断他和之前的点哪个对于i来说更优
我们考虑用单调队列来决策最优解
考虑两种情况
1)单调队列队首的元素q[head]和q[head+1],如果这两个点连线的斜率小于当前的斜率2s[i]2∗s[i],那说明当前head没有head+1来的优,又因为我们在枚举i的过程中2s[i]2∗s[i]是单调增的,所以head永远不会成为最优解,所以head可以出队
重复这个过程直到队列中小于2个元素或当前q[head]和q[head+1]的连线的斜率大于2s[i]2∗s[i],那么当前head比head+1来的优. 又因为我的单调队列里面维护的是斜率不断递增的一些直线(原因会在2中阐述),所以head+1又比head+2要来的优,以此类推,当前head就是最优决策,我们可以用head对应的dp值来更新dp[i]
2)考虑将当前点dp[i]插入单调队列,我们考虑单调队列的末尾两个元素q[tail-1]和q[tail],设q[tail-1]和q[tail]之间连线的斜率为k1,q[tail]和i之间连线的斜率为k2.如果k1>k2k1>k2,我们就直接删去tail节点,因为它永远不可能成为最优决策
考虑用反证法来证明这个结论:如果tail在某个k节点的阶段中成为最优决策,那么根据上面的式子应该有结论2s[k]<k22∗s[k]<k2;然而因为k2<k1k2<k1,所以2s[k]<k12∗s[k]<k1,所以我的单调队列不会将tail-1弹出,最优决策点会是tail-1或更早的点,矛盾
有了这两个结论我们就可以用单调队列解决这个问题。每个元素进队一次出队一次,总复杂度O(n)O(n)

我们再尝试从几何的角度来考虑这个问题
之前的状态转移方程:

dp[i]=min(dp[j]+s[i]2+(s[j]+L)22s[i](s[j]+L))dp[i]=min(dp[j]+s[i]2+(s[j]+L)2−2∗s[i]∗(s[j]+L))

换个形式
dp[i]+2s[i](s[j]+L)=dp[j]+s[i]2+(s[j]+L)2dp[i]+2∗s[i]∗(s[j]+L)=dp[j]+s[i]2+(s[j]+L)2

联想直线的点斜式y=kx+by=kx+b,这里我们把dp[i]dp[i]看做bb,把2s[i]看做kk,把dp[j]+s[i]2+(s[j]+L)2看做yy
对每个j,我们建立点(s[j]+L,dp[j]+s[i]2+(s[j]+L)2)
这样,我们在决策点i的时候,相当于过之前已经出现的所有点画斜率为2s[k]2∗s[k]的直线,求其中截距最小的一条
考虑到我在建关于j的点的时候不可能出现s[i]s[i],但考虑到在任意时刻,把所有的点的纵坐标同时加上或减去一个数不会影响我最后的决策,所以我们可以把s[i]2s[i]2扔掉
之前的表述还不够直观,我们还可以这样想:决策i这个点时,我们拿着一条斜率为2s[i]2∗s[i]的直线从y轴负无穷向上平移,最先碰到的那个点一定满足截距最小,就是我需要的最优决策点
这样我们在代数部分难以理解的两个结论就很好想了
对于1)如果q[head]和q[head]+1之间的连线的斜率小于2s[i]2∗s[i],那么我的直线肯定会先碰到head+1,再考虑到2s[i]2∗s[i]的单增性,head将永远不会成为最优决策点
对于2)设q[tail-1]和q[tail]之间连线的斜率为k1,q[tail]和i之间连线的斜率为k2,如果k1>k2,在形状上看相当于tail这个点凹进去了,那么我的直线平移的时候肯定不会先碰到这个点
事实上,我们的单调队列维护的是当前所有点的一个下凸壳,随着2s[i]2∗s[i]的递增,我们不断删去下凸壳左边的部分并向右边添加新点并构造下凸壳的新部分,这就是单调队列的实质
#include <cstdio>
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <utility>
#include <cctype>
#include <algorithm>
#include <bitset>
#include <set>
#include <map>
#include <vector>
#include <queue>
#include <deque>
#include <stack>
#include <cmath>
#define LL long long
#define LB long double
#define x first
#define y second
#define Pair pair<LL,LL>
#define pb push_back
#define pf push_front
#define mp make_pair
#define LOWBIT(x) x & (-x)
using namespace std;

const int MOD=100003;
const LL LINF=2e16;
const int INF=1e9;
const int magic=348;
const double eps=1e-10;

inline int getint()
{
    char ch;int res;bool f;
    while (!isdigit(ch=getchar()) && ch!='-') {}
    if (ch=='-') f=false,res=0; else f=true,res=ch-'0';
    while (isdigit(ch=getchar())) res=res*10+ch-'0';
    return f?res:-res;
}

int n;LL L;
int c[100048];
LL sum[100048],s[100048],dp[100048];

struct node
{
    LL x,y;
    int ind;
}q[100048];int head,tail;

inline double calc_k(node x,node y)
{
    return double(y.y-x.y)/(y.x-x.x);
}

int main ()
{
    int i,curk;
    n=getint();L=getint()+1;sum[0]=0ll;
    for (i=1;i<=n;i++) c[i]=getint(),sum[i]=sum[i-1]+c[i],s[i]=sum[i]+i;
    q[1]=node{L,L*L,0};head=tail=1;memset(dp,0,sizeof(dp));
    for (i=1;i<=n;i++)
    {
        curk=2*s[i];
        while (head<tail && calc_k(q[head],q[head+1])<curk) head++;
        dp[i]=dp[q[head].ind]+(s[i]-s[q[head].ind]-L)*(s[i]-s[q[head].ind]-L);
        node ins=node{s[i]+L,dp[i]+(s[i]+L)*(s[i]+L),i};
        while (head<tail && calc_k(q[tail-1],q[tail])>calc_k(q[tail],ins)) tail--;
        q[++tail]=ins;
    }
    printf("%lld\n",dp[n]);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值