斜率优化 学习笔记
前言:寒假Yousiki讲过斜率优化,但完全没有听懂。现在文化课解析几何也学了不少,终于能做一些题了。
-----------------
有时候我们列出DP方程会得到形如这样的式子:
$f[i]=max/min{f[j]+(a[i]-b[j])^2}+w[i](1\leq j<i)$(其实平不平方都随便啦)这个式子复杂度是$O(n^2)$的。
我们变换一下形式:
$2*a[i]*b[j]+f[i]-a[i]^2-w[i]=f[j]+b[j]^2$
仔细一看,上面的等式可以化成$y=kx+b$的形式,即含$j$的项视为$x$和$y$,含$i$的项视为常数项。
根据这个式子,我们可以将转移优化成$O(n)$的。
根据$y=kx+b$的形式,我们可以视为平面上有许多点,坐标为$(b[j],f[j]+b[j]^2)$。这些点连起来可以形成一个凸包。我们用单调队列来维护这个凸包。
假设$k=2*a[i]>0$,即维护下凸包,斜率是单调递增的。下面的讲述可以用线性规划来理解(人教版高中数学必修二)。
1.如果经过某两点直线的斜率小于现在直线的斜率,那么$head++$。因为我们现在的直线肯定是想经过凸包的边界的,而现在的答案肯定不是最优。
2.现在的队头就是最优解。把$q[head]$带到原方程中。
3.如果经过$q[tail]$和$q[tail-1]$的直线的斜率大于经过$q[tail-1]$和$i$的斜率,那么$tail--$。因为凸包肯定是要囊括所有点的,不能让有的点在凸包外面。
于是转移被优化成$O(n)$的了。
怎么样?是不是很简单?滑稽
其实学长教我们的时候推荐我们用叉积来判断关系,因为会有些极端数据卡掉斜率(比如斜率不存在)。但大部分题都不卡斜率这种做法。
-------------------------------
例题 【HNOI2008】玩具装箱
斜率优化入门题。
设$f[i]$表示考虑前$i$个玩具所花费的最小费用,很容易得出方程$f[i]=\min{f[j]+((i-j-1-L)+\sum\limits_{k=i-j+1}^i c[k])^2}$
前缀和优化,变换一下形式:$f[i]=\min(f[j]+(sum[i]-sum[j]+i-j-1-L)^2)$
设$a[i]=sum[i]+i$,$b[i]=sum[i]+i+L+1$
则$f[i]=\min(f[j]+(a[i]-b[j])^2)$
化成$y=kx+b$的形式:$2*a[i]*b[j]+f[i]-a[i]^2=f[j]+b[j]^2$
然后斜率优化一下就可以了。
代码:
#include<bits/stdc++.h> #define int long long using namespace std; int dp[50005],n,L,x; double sum[50005]; int Q[50005],head,tail; double a(int x) {return sum[x]+x;} double b(int x){return a(x)+L+1;} double X(int x){return b(x);} double Y(int x){return dp[x]+b(x)*b(x);} double slope(int i,int j){return (Y(i)-Y(j))/(X(i)-X(j));} signed main() { scanf("%d%d",&n,&L); for(int i=1;i<=n;i++){ scanf("%lf",&sum[i]); sum[i]+=sum[i-1]; } head=tail=1; for(int i=1;i<=n;i++){ while(head<tail&&slope(Q[head],Q[head+1])<2*a(i)) ++head; dp[i]=dp[Q[head]]+(a(i)-b(Q[head]))*(a(i)-b(Q[head])); while(head<tail&&slope(i,Q[tail-1])<slope(Q[tail-1],Q[tail])) --tail; Q[++tail]=i; } printf("%lld",dp[n]); return 0; }