斜率优化DP学习笔记
RT,最近练习一些斜率优化的DP,也发现了不少的问题,下面进行一下小小的记录与知识的回顾与理解
介绍
首先,斜率优化是一种对DP进行优化的好东西,大多数的时候,它的使用可以把 O(n2) 的复杂度降低到 O(nlogn) 甚至是 O(n) ,提升的效果十分明显,已经渐渐地成为一种特定的题目类型
原理
它的原理,基于数形结合的思想。有时候,我们会遇到一些DP题目,而这些题目有一个特点,就是它的决策从前面已经求出来DP值的状态(即前驱状态)中进行转移(其实这句话和废话一样)然后,我们发现它们的表达式是这样的形式:
F(now)=F(pre)+K(pre)+A
其中
F(now)
是现在正在考虑的状态,
F(pre)
是它的前驱状态,
K(pre)
是一个只与
K
有关的函数,而
为了知道我们选取哪一个状态进行计算结果最优,我们可以做如下的作差比较,考虑前驱状态 x,y 我们进行作差:
F(x)+K(x)+A−(F(y)+K(y)+A)
=F(x)+K(x)−F(y)−K(y) (*)
如果
x
要比
F(x)−F(y)>K(y)−K(x)
不妨令
x>y
,则我们可以这样(其实往往我们可以证明出
K
函数的单调性,或是它含有单调的因式,这样我们可以放心地把它除过去,这里不妨设
(F(x)−F(y))/(K(y)−K(x))>1
这时,我们类比解析几何里两点斜率的定义,可以这样进行点的创建:
上述式子表示的就是 A(−K(x),F(x)),B(−K(y),F(y)) 的连线的斜率
当然上述式子比较狭义,广义的来说是可以通过各种手段来做到这一步就可以:
(F(x)+K(x)−F(y)−K(y))/(G(x)−G(y))>A
其中
K(i),G(i)
函数是只与
i
有关的函数,然后,我们可以通过用单调栈的方法维护上凸壳或下凸壳来进行快速的检索,可以使用双指针,即再用一个指针记录当前最优解的位置(此时要求最优解的单调性),此时复杂度为
应用
下面贴几道例题,当然由于我非常弱,所以持续更新
BZOJ 1010: [HNOI2008]玩具装箱toy
经典题,没有什么好说的,采用上文所述的广义的方法即可,丑陋的代码如下
#include<cstdlib>
#include<cstdio>
#include<algorithm>
#define maxn 50005
#define LL long long int
#define A (LL)(2*i+2*pre[i]-2*l-2)
#define EFF (LL)(i-op.idx+pre[i]-pre[op.idx]-l-1)*(i-op.idx+pre[i]-pre[op.idx]-l-1)
#define set_y(k) (LL)(dp[k]+(k+pre[k])*(k+pre[k]))
#define set_x(k) (LL)(k+pre[k])
#define slope(i,j) (double)(i.y-j.y)/(i.x-j.x)
using namespace std;
struct point{
LL x,y,idx;
void read(LL x,LL y,LL idx){
this->x=x,this->y=y,this->idx=idx;
}
}stack[maxn];
LL n,l,top=0,now=0;
LL len[maxn],dp[maxn],pre[maxn];
void DP(){
dp[0]=0;
stack[0].read(0,0,0);
for(LL i=1;i<=n;i++){
point op=stack[now];
while(top>0){
if(now<top&&slope(stack[now],stack[now+1])<A){
op=stack[++now];
continue;
}
break;
}
dp[i]=dp[op.idx]+EFF;
point next;
next.read(set_x(i),set_y(i),i);
while(top>0){
op=stack[top];
if(top>0&&slope(op,next)<slope(stack[top-1],op)){
top--;
if(now>top)now--;
continue;
}
break;
}
stack[++top]=next;
}
printf("%lld",dp[n]);
}
int main(){
/*freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);*/
scanf("%lld%lld",&n,&l);
for(LL i=1;i<=n;i++){
scanf("%lld",&len[i]);
}
pre[1]=len[1];
for(LL i=2;i<=n;i++){
pre[i]=pre[i-1]+len[i];
}
DP();
return 0;
}
细节
1.最好在单调栈中最开始加一个虚拟点(原点),处理起来更方便,同时数组从1开始编号,有可能溢出时使用long long int类型
2.要看好维护的是上凸壳还是下凸壳,这两种的判断符号不同,而且最优解指针要注意不能越界
3.要输出最终解。。。