【详解】斜率优化 DP + 凸包
凸包和凸包的维护
凸包的概念
先来了解凸包的概念。
凸包是一个类似于下面这样的东西。
顾名思义,它是凸的(一定是最凸的)。
这是一个下凸包,当然也有上凸包。
它是有多个点连成的边组成的。
凸包的维护
对于一个下凸包,我们可以发现它的每条边的斜率是递增的,于是我们可以用单调队列来维护一个凸包。
比如说,我们现在要将第 个点加入这个凸包,我们只需要判断第 个点和第 个点连成的线的斜率和 与 形成的边的斜率的大小关系。
如果新加入的点 与 连成的线段的斜率比原来的要小(如上图所示),那说明它可以与 形成更优的凸包,所以 tail--,以此往复。
如果新加入的点 与 连成的线段的斜率比原来的要大(如上图所示),那说明它可以加入到这个凸包之中,直接将 入队。
同理,上凸包也一样,可以自己推推……
维护凸包局部代码:
#include<bits/stdc++.h>
using namespace std;
int x[100001],y[100001];//x[i]为第i个点的x坐标,y为第i个点的y坐标
double slope(int i,int j)//计算斜率
{return (y[j]-y[i])/(x[j]-x[i]);
}
int q[100001],n;
int main()
{int head=0,tail=1;for(int i=1;i<=n;i++){while(head<tail&&slope(q[tail],i)<=slope(q[tail-1],q[tail]))//head<tail,至少要两个点才能算斜率 tail--;q[++tail]=i;}
}
斜率优化 DP
ok, 讲完了凸包,接下来来讲斜率优化。
先以一个例题来讲吧……
例题:HDU 3507 Print Article
传送门https://acm.hdu.edu.cn/showproblem.php?pid=3507vjudge 传送门https://vjudge.net/problem/HDU-3507题目大意:
解题思路
设 表示打印前 个文章的最小成本。
易得:
其中 , 为 的前缀和。
显然这样的转移是 的,那是包会超时的好吧!
优化
所以,我们得考虑优化……
先拆解这个转移方程,先不管 。
将平方项拆开,得:
将 看为未知数, 看为常数,把只含 的项移到等号左边,得:
两边同时乘 -1,得:
这时,你觉得它像啥?
对,没错,就是
我们将转移方程与 对比,可以得到:
于是,对于每一个 ,我们可以把它抽象成二维平面上的点,每个点的坐标是 。
那是不是对于每一个 ,我们都可以找前面的一个点 转移。
然后对于每一个点 ,都是不是会有一条 的直线经过它?
然后对于每一个点 ,都会有一条过它的斜率为 的直线,都会有一个截距 。
所以,就是要找一个点 ,使得过这个点的斜率为 的直线的截距最小,这样我们的 也就会最小了。
那你想想,这个点满足什么特性?
没错,它就是在下凸包上!
可是下凸包上有很多个点,该选哪个呢?
你觉得是 ①,②还是③?
由图像,我们可以发现①的截距是最小的?
那什么时候是最小的呢?
就是这个凸包上的点相邻的两条边的斜率刚好一条小于绿色线的斜率,一条大于绿色线的斜率。
所以,你可以二分查找凸包上的点。
当然,这题并没有必要,因为你可以发现 是单调递增的,也就是 越大, 对应的斜率就越大。
也就是越往后,我们只会用到上一个 用的之后的点。
所以可以一边入队,一边出队。
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
int sum[500011];
int f[500011];
int q[500011];
int dx(int i,int j){return sum[i]-sum[j];
}
int dy(int i,int j)
{int y1=f[i]+sum[i]*sum[i];int y2=f[j]+sum[j]*sum[j];return y1-y2;
}
signed main()
{// ios::sync_with_stdio(false);// cin.tie(0);// cout.tie(0);while(scanf("%lld%lld",&n,&m)!=EOF){sum[0]=0;f[0]=0;for(int i=1;i<=n;i++){scanf("%lld",&sum[i]);sum[i]+=sum[i-1];}memset(q,0,sizeof q);memset(f,0,sizeof f);int head=1,tail=0;for(int i=1;i<=n;i++){while(head<tail&&dy(i-1,q[tail])*dx(q[tail],q[tail-1])<=dx(i-1,q[tail])*dy(q[tail],q[tail-1]))tail--;q[++tail]=i-1;while(head<tail&&dy(q[head+1],q[head])<=dx(q[head+1],q[head])*2*sum[i])head++;f[i]=f[q[head]]+(sum[i]-sum[q[head]])*(sum[i]-sum[q[head]])+m;}printf("%lld\n",f[n]);}return 0;
}
总结
斜率优化主要用于转移方程中有既含有 又含有 的项。
当斜率 k 和 x 坐标都递增时,直接单调队列维护凸包 (参考 [HNOI2008] 玩具装箱);
当斜率 k 不递增且 x 坐标递增时,需要二分凸包上的点(参考 [SDOI2012] 任务安排);
当斜率 k 不递增且 x 坐标也不递增时,需要使用二分 + CDQ 分治 / 平衡树(后续可能会写);
最后求赞勿喷。