这几天一直在搞《1D 1D dp优化初步》上的内容,先感谢作者大犇一波。
这个专题主要整理最近这几天整理的斜率优化dp的做题心得和题目讲解。
(现在把下面的题目都拆开来了)
能用斜率优化的dp题一般都需要证明决策单调性(然而我到现在还没有搞懂到底何为决策单调)。一般有几种思路:
1) 将得到的数据打表,一般打表打 [L,R] 内这一段的值,判断是否基本满足四边形不等式:
2) 在敲暴力的过程中能感觉到,需要从前面已经得到的结果查找最优解,但是这个最优解在一定范围内,而不必全部找过来的时候,就可能需要用上优化了。
3)
重要的是如果能写出《1D 1D》中的经典模型的形式:
f(x)=minx−1i=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
从
3)
为了使队列中的
deq[R]
弹出,就需要
deq[R]
对于
deq[R−1]
和
i
都不优:
设
因为当 g(deq[R−1],deq[R])≤w[i] 时, deq[R−1] 更优;当 g(deq[R−1],deq[R])>w[i] 时, g(dep[R],i)>w[i] , i 更优。
所以一个斜率优化的dp的重点在于不是很喜直线方程的写法)以及两个缩进while语句的符号问题(反正只要考虑左侧的要求能够传递到右侧而不会改变即可)。
BZOJ1010 HNOI2008 玩具装箱toy
Task :
Description
P教授要去看奥运,但是他舍不下他的玩具,于是他决定把所有的玩具运到北京。他使用自己的压缩器进行压缩,其可以将任意物品变成一堆,再放到一种特殊的一维容器中。
P教授有编号为1…N的N件玩具,第i件玩具经过压缩后变成一维长度为Ci。为了方便整理,P教授要求在一个一维容器中的玩具编号是连续的。同时如果一个一维容器中有多个玩具,那么两件玩具之间要加入一个单位长度的填充物。
形式地说如果将第i件玩具到第j个玩具放到一个容器中,那么容器的长度将为
x=j−i+∑k=ijCk制作容器的费用与容器的长度有关,根据教授研究,如果容器长度为x,其制作费用为 (x−L)2 ,其中L是一个常量。P教授不关心容器的数目,他可以制作出任意长度的容器,甚至超过L。但他希望费用最小。Sample Input & Output
5 4
3 4 2 1 41
Solution
:
这道题是我第一道认真研究的斜率优化dp。按照之前讲过的方法可以得到转移方程:
其中 定义w[i,j]表示在区间[i+1,j]内的值,
暴力的复杂度是 O(n2) 。
接着我们先打印出所有的状态表来:
L\R | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
0 | 1 | 16 | 49 | 81 | 196 |
1 | 0 | 9 | 25 | 100 | |
2 | 4 | 0 | 25 | ||
3 | 9 | 4 | |||
4 | 0 |
我们可以得到满足样例的结果是分成{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)
函数:
∵
∴
最后得到:
此处还要注意有两种写法,一种是采用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;
}