斜率优化dp专题 & BZOJ1010 HNOI2008 玩具装箱toy


这几天一直在搞《1D 1D dp优化初步》上的内容,先感谢作者大犇一波。
这个专题主要整理最近这几天整理的斜率优化dp的做题心得和题目讲解。
现在把下面的题目都拆开来了


能用斜率优化的dp题一般都需要证明决策单调性(然而我到现在还没有搞懂到底何为决策单调)。一般有几种思路:

1) 将得到的数据打表,一般打表打 [L,R] 内这一段的值,判断是否基本满足四边形不等式:

w[i,j]+w[i+1,j+1]w[i+1,j]+w[i,j+1]
的模样,或者还有敲暴力之后看看dp数组内的值是否单调。

2) 在敲暴力的过程中能感觉到,需要从前面已经得到的结果查找最优解,但是这个最优解在一定范围内,而不必全部找过来的时候,就可能需要用上优化了。

3) 重要的是如果能写出《1D 1D》中的经典模型的形式: f(x)=minx1i=1{f(i)+w[i,x]} 那几乎就可以确定出题人就在考斜率优化dp。
然而最快捷的方法还是要鬼神级的YY


一旦确定这题可以用斜率优化dp做,那么题目就变成了板子题。按我最近刷这类题目的方法来看,其实只要拆分成下面几步就可以做:

1) 对题目进行单调性分析并且列出转移方程式。由于最近刷的题目早就知道是斜率优化所以没有分析单调性,而单调性的得出还是需要转移方程式辅助。

由于 O(n) 的平摊复杂度需要用到单调队列deque,我们需要针对缩进front指针和rear指针的两个while操作进行设计函数:(就以上面提及的标准函数为例)

2) 为了使队列中的 deq[L] 弹出,就需要 deq[L] 不比 dep[L+1] 优:
i,j,k 满足条件 j<k<i ,且 i j转移比从 k 转移优,则可以列出等式:

dp[j]+w[j,i]dp[k]+w[k,i]
通过对该式子分离出带有i的变量,可以得到一个解题最重要的不等式(斜率优化的代码辣么短都是因为这个不等式的关系)。如果提取不出上述要求的不等式就不能用斜率优化dp解题。最后就能精简出一个函数 g(x,y) 满足 g(x,y)w[i] 。但是我并不建议实际代码写成 g(x,y)

3) 为了使队列中的 deq[R] 弹出,就需要 deq[R] 对于 deq[R1] i 都不优:
a,b,c满足条件 a<b<c<i ,如果满足 g(deq[R1],deq[R])g(deq[R],i) ,则 deq[R] 不优。

因为当 g(deq[R1],deq[R])w[i] 时, deq[R1] 更优;当 g(deq[R1],deq[R])>w[i] 时, g(dep[R],i)>w[i] i 更优。

所以一个斜率优化的dp的重点在于g(x,y)的推导(不是很喜直线方程的写法)以及两个缩进while语句的符号问题(反正只要考虑左侧的要求能够传递到右侧而不会改变即可)。


BZOJ1010 HNOI2008 玩具装箱toy

Task

Description

P教授要去看奥运,但是他舍不下他的玩具,于是他决定把所有的玩具运到北京。他使用自己的压缩器进行压缩,其可以将任意物品变成一堆,再放到一种特殊的一维容器中。

P教授有编号为1…N的N件玩具,第i件玩具经过压缩后变成一维长度为Ci。为了方便整理,P教授要求在一个一维容器中的玩具编号是连续的。同时如果一个一维容器中有多个玩具,那么两件玩具之间要加入一个单位长度的填充物。

形式地说如果将第i件玩具到第j个玩具放到一个容器中,那么容器的长度将为

x=ji+k=ijCk
制作容器的费用与容器的长度有关,根据教授研究,如果容器长度为x,其制作费用为 (xL)2 ,其中L是一个常量。P教授不关心容器的数目,他可以制作出任意长度的容器,甚至超过L。但他希望费用最小。

Sample Input & Output

5 4
3 4 2 1 4

1

Solution
这道题是我第一道认真研究的斜率优化dp。按照之前讲过的方法可以得到转移方程:

dp[i]=minj=1i1{dp[j]+w[j,i]}

其中 定义w[i,j]表示在区间[i+1,j]内的值
w[i,j]=(ji1+sum[R]sum[L]Len)2

暴力的复杂度是 O(n2)
接着我们先打印出所有的状态表来:

L\R12345
01164981196
10925100
24025
394
40

我们可以得到满足样例的结果是分成{1},{2},{3,4},{5}四组,并且我们也可以发现这个表格是满足四边形不等式的。

分析完单调性,我们考虑不同的时间复杂度来完成这道题:
1) O(logn)
首先考虑采用 O(logn) 的单调栈完成这个题目。我们考虑当前的dp(i)能够去更新哪些点,那么能够更新的点就会形成一段区间。

我们在栈中存储的值是每个不同的dp(i)所能更新的区间左端点,而右端点则是接下来的一个左端点(栈顶的右端点是n),可更新的最优区间就是 [stk[i],str[i+1])

查找结果的时候,我们只需要用二分查找找到包含该位置的最优区间;更新最优解的时候,始终考虑以该点作为更新点,可以更新到的离n的最远点在哪里。如果比当前的区间还优(即用当前区间的更新点不比用dp[i]优),将其覆盖;否则只会优于当前区间的部分区间,满足01性,同样通过二分查找可以找到结果。平摊的复杂度最大就是 O(logn)

我写 O(nlogn) 的时候反而比 O(n) 卡了更久,没有搞明白区间和更新点之间的关系,但是搞明白之后对单调性有更清楚的认识。

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define M 50005
int n,Len,stk[M],pre[M],top=0;
long long sum[M],dp[M];
long long sqr(long long x){return x*x;}
long long calc(int L,int R){return dp[L]+sqr(R-L-1+sum[R]-sum[L]-Len);}
int main(){
    scanf("%d %d",&n,&Len);
    for(int i=1,x;i<=n;i++){
        scanf("%d",&x);
        sum[i]=sum[i-1]+x;
    }

/*     
    for(int i=0;i<n;i++){
        for(int j=i+1;j<=n;j++)
            printf("[%d,%d]%lld ",i,j,calc(i,j));
        puts("");
    }//打印出w[i,j],观察答案与单调性
*/ 

    dp[0]=0;stk[++top]=0;pre[top]=0;
    for(int i=1;i<=n;i++){
        int L=1,R=top,res=0;
        while(L<=R){
            int mid=L+R>>1;
            if(stk[mid]<=i){
                res=mid;
                L=mid+1;
            }else R=mid-1;
        }
        dp[i]=calc(pre[res],i)

        while(top&&i<stk[top]&&calc(pre[top],stk[top])>calc(i,stk[top]))--top;     
        L=stk[top],R=n,res=n+1;
        while(L<=R){
            int mid=L+R>>1;
            if(calc(pre[top],mid)>calc(i,mid)){
                res=mid;
                R=mid-1;
            }else L=mid+1;
        }

        if(res!=n+1){//当i根本不优的时候不能插入i
            stk[++top]=res;
            pre[top]=i;
        }
    }
    printf("%lld\n",dp[n]);
}

2) O(n)
尝试推一下 g(x,y) 函数:

dp[j]+(f[i]f[j]c)2dp[k]+(f[i]f[k]c)2
使得 i 优于j。此时 f[k]=sum[k]+k,c=Len+1
dp[j]dp[k]+(f[j]+c)2(f[k]+c)2<=2f[i](f[j]f[k])

最后得到:
g(j,k)=dp[j]+(f[j]+c)2dp[k](f[k]+c)22(f[j]f[k])<=f[i]

此处还要注意有两种写法,一种是采用double的 g(x,y) 写法,一种是将分子分母分离交叉相乘的写法,但是 g(x,y) 写法的缺点是会出现 分母为0的情况需要特判,一个不小心还会出现精度问题。从速度上来讲,用除法显然比用乘法慢(在BZOJ上亲测用 g(x,y) 慢了50+ms)。

/* g(x,y)写法 */
#define M 50005
int n,Len;
long long dp[M],sum[M];
long long sqr(long long x){return x*x;}
double g(int x,int y){
    return (dp[y]+sqr(sum[y]+y+Len+1)-dp[x]-sqr(sum[x]+x+Len+1))/(2.0*(sum[y]-sum[x]+y-x));
}
int deq[M],L=0,R=-1;
int main(){
    scanf("%d %d",&n,&Len);
    for(int i=1,x;i<=n;i++){
        scanf("%d",&x);
        sum[i]=sum[i-1]+x;
    }
    deq[++R]=0;
    for(int i=1;i<=n;i++){
        while(L<R&&g(deq[L],deq[L+1])<=sum[i]+i)++L;
        dp[i]=dp[deq[L]]+sqr(sum[i]-sum[deq[L]]+i-deq[L]-1-Len);
        while(L<R&&g(deq[R-1],deq[R])>g(deq[R],i))--R;
        deq[++R]=i;
    }
    printf("%lld\n",dp[n]);
    return 0;
}

/*  分离分子分母的写法   */
#define M 50005
int n,Len;
int deq[M],L=0,R=-1;
long long dp[M],sum[M];
long long sqr(long long x){return x*x;}
long long up(int x,int y){return dp[y]-dp[x]+sqr(sum[y]+y+Len+1)-sqr(sum[x]+x+Len+1);}
long long down(int x,int y){return 2*(sum[y]-sum[x]+y-x);}
int main(){
    scanf("%d %d",&n,&Len);
    for(int i=1,x;i<=n;i++){
        scanf("%d",&x);
        sum[i]=sum[i-1]+x;
    }
    deq[++R]=0;
    for(int i=1;i<=n;i++){
        while(L<R&&up(deq[L],deq[L+1])<=(sum[i]+i)*down(deq[L],deq[L+1]))++L;
        dp[i]=dp[deq[L]]+sqr(sum[i]-sum[deq[L]]+i-deq[L]-1-Len);
        while(L<R&&up(deq[R-1],deq[R])*down(deq[R],i)>up(deq[R],i)*down(deq[R-1],deq[R]))--R;
        deq[++R]=i;
    }
    printf("%lld\n",dp[n]);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值