[模板] 斜率优化dp详解

本文介绍了斜率优化DP的概念及其实现方式,并通过一个具体题目(hdu3507)进行实例分析,展示了如何利用斜率优化DP解决特定类型的问题。

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

转自大佬:https://blog.youkuaiyun.com/bill_yang_2016/article/details/54667902

 

算法简介

今天xinyue讲了斜率优化,全程懵逼,居然还有这么牛逼的东西。 
于是与achen讨论了一下,总结一些东西。 
斜率优化Dp其实是单调队列的推广,单调队列、旋转卡壳、斜率优化都利用了单调性降低时间复杂度。


算法简介

举个例子 
有些动规状态转移方程可以写成 
f[i]=min/max{f[j]+…+x[i]},省略号中只有与j有关的变量。 
我们就可以用单调队列进行优化,使O(n^2)降为O(n) 
但是对于这一类的方程: 
f[i]=min/max{f[j]+(x[i]-x[j])^2} 
展开后得到 
f[i]=min/max{f[j]+x[i]^2+x[j]^2-2*x[i]*x[j]
红色部分不仅有j相关的量,还有与i有关的量,从而使单调队列失效。 
此时我们就需要用到斜率优化。


引例

hdu3507 
题目大意: 
给出N个单词,每个单词有个非负权值Ci,现要将它们分成连续的若干段,每段的代价为此段单词的权值和,还要加一个常数M,即(∑Ci)^2+M。现在想求出一种最优方案,使得总费用之和最小。

容易写出方程: 
f[i]=min{f[j]+(s[i]-s[j])^2+M}(0<=j<=i-1) 
其中s是前缀和 
可是范围是500000,又不能用单调队列,那怎么办呢?


算法核心

数学分析法见此大爷博客,讲的挺详细->传送门 
以下谈谈斜率优化的数形结合理解方法: 
以下纯粹空谈,请结合引例分析理解。

根据动规方程状态i从状态j转化而来, 
我们可以化成类似f[i]=(f[j]+…)+(-i*f[i-1]*f[j])+(i+…)的形式 
其中蓝色部分只与j有关红色部分与i,j有关绿色部分只与i或常数有关 
我们可以固定i,故变形为 
f[i]-i-…=(f[j]+…)+(-i*f[i-1]*f[j]) 
考虑几何意义, 
蓝色部分为y红色部分中的i部分为导数k,红色部分中j部分为x绿色部分只是常数,在几何意义上只是截距,与单调性无关,可以设为B 
故得y=kx-B 
是不是很像直线方程? 
假设关于i的部分>0且随着i增加而增加,求的是最小值 
则k随着i增加而增加,对于有效点集我们可以画出下图。 
这里写图片描述
是不是很像一个下凸包? 
我们用当前的斜率k从下方去不断逼近下凸包,最终会先碰到哪一个点? 
这里写图片描述
一定是与斜率最相近的点,因为函数单调递增,故小于斜率的决策肯定没有后面的优,舍去。 
因此我们可以用一个类似单调队列的双端队列来维护状态j,以下是实现方案:

若导数小于当前斜率,舍掉队首。 
根据方程使用队首取出j算出当前f[i]的值 
接着我们要加入结点i,但还得维护队列的下凸性,如果加入结点i破坏了下凸性,就弹去队尾,直到下凸位置(请dalao们不要吐槽示意图,没时间改了) 
这里写图片描述
因此可以得到O(n)的算法

当然,若方程关于i的部分随着i增加而减小,且>0,则维护上凸性。以此类推,树形结合分析。

核心代码如下:

int Left=1,Right=1;
    Q[1]=0;
    f[0]=0;
    for(int i=1; i<=n; i++) {
        while(Left<Right&&Slope(Q[Left],Q[Left+1])<=sumd[i])Left++; //维护队首(删除非最优决策)
        int Front=Q[Left]; //取出队列中最优元素j 
        f[i]=Cal(i,Front); //根据方程计算当前f
        while(Left<Right&&Slope(Q[Right-1],Q[Right])>=Slope(Q[Right],i))Right--; //维护队尾(维护下凸包性质)
        Q[++Right]=i; //入队
    }

引例分析

f[i]=min{f[j]+(s[i]-s[j])^2+M} 
展开得 
f[i]=min{f[j]+s[i]^2+s[j]^2-2*s[i]*s[j]+M} 
令f[i]=B,f[j]+s[j]^2=y,2*s[j]=x,k=s[i] 
因此k*x+B=y 
k是s[i],前缀和随着i增大而增大,因为求最小值,故维护下凸包。

#include<algorithm>
#include<iostream>
#include<iomanip>
#include<cstring>
#include<cstdlib>
#include<vector>
#include<cstdio>
#include<cmath>
#include<queue>
using namespace std;
inline const int Get_Int() {
    int num=0,bj=1;
    char x=getchar();
    while(x<'0'||x>'9') {
        if(x=='-')bj=-1;
        x=getchar();
    }
    while(x>='0'&&x<='9') {
        num=num*10+x-'0';
        x=getchar();
    }
    return num*bj;
}
long long n,m,Q[500005],f[500005],s[500005];
double Slope(long long j,long long k) { //求斜率 
    return double((f[j]+s[j]*s[j])-(f[k]+s[k]*s[k]))/(2*s[j]-2*s[k]);
}
int main() {
    while(scanf("%lld%lld",&n,&m)!=EOF) {
        for(int i=1; i<=n; i++)s[i]=s[i-1]+Get_Int();
        int Left=1,Right=1;
        Q[1]=0;
        f[0]=0;
        for(int i=1; i<=n; i++) {
            while(Left<Right&&Slope(Q[Left],Q[Left+1])<=s[i])Left++; //维护队首(删除非最优决策) 
            int Front=Q[Left];
            f[i]=f[Front]+(s[i]-s[Front])*(s[i]-s[Front])+m; //计算当前f 
            while(Left<Right&&Slope(Q[Right-1],Q[Right])>=Slope(Q[Right],i))Right--; //维护队尾(维护下凸包性质) 
            Q[++Right]=i; //入队 
        }
        printf("%lld\n",f[n]);
    }
    return 0;
}

经典例题

[hdu3507] 打印文章(引例) 
[HNOI2008] 玩具装箱 
[CEOI2004] 锯木厂选址 
[ZJOI2007] 仓库建设 
[USACO 2008 March Gold] 土地购买 
[APIO2010] 特别行动队 
[APIO2014] 序列分割 
[bzoj3437] 小P的牧场 
[SDOI2016] 征途

这里留下的坑慢慢填吧,希望填的完。


后记

斜率优化这一部分比较难懂,读者可以自己在纸上推算一下。 
如果有疑问或者认为本文有问题请在下面↓提出,感谢大家的支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值