斜率优化DP学习笔记

本文介绍了斜率优化动态规划(DP)的概念,通过数形结合的思路解析其原理,指出这种优化方法能将复杂度从O(n^2)降低到O(nlogn)甚至O(n)。详细讨论了如何通过维护上凸壳或下凸壳,利用单调栈或双指针、二分查找等方法实现。并给出了BZOJ 1010:[HNOI2008]玩具装箱toy的案例分析,强调了在实际应用中的一些细节处理,如单调栈初始虚拟点、数组编号和数据类型选择等。
摘要由CSDN通过智能技术生成

斜率优化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 有关的函数,而A是一个常数

为了知道我们选取哪一个状态进行计算结果最优,我们可以做如下的作差比较,考虑前驱状态 x,y 我们进行作差:

F(x)+K(x)+A(F(y)+K(y)+A)

=F(x)+K(x)F(y)K(y) (*)

如果 x 要比y更优,则我们会知道(*)式 >0 ,即:

F(x)F(y)>K(y)K(x)

不妨令 x>y ,则我们可以这样(其实往往我们可以证明出 K 函数的单调性,或是它含有单调的因式,这样我们可以放心地把它除过去,这里不妨设K(x)<K(y)):

(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 有关的函数,然后,我们可以通过用单调栈的方法维护上凸壳或下凸壳来进行快速的检索,可以使用双指针,即再用一个指针记录当前最优解的位置(此时要求最优解的单调性),此时复杂度为O(n),或二分查找,此时复杂度为 O(nlogn) ,那么它的原理就这样结束啦O(∩_∩)O~~

应用

下面贴几道例题,当然由于我非常弱,所以持续更新

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.要输出最终解。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值