小引
在学习斜率优化之前,大家的教练应该都教过一些关于dp的小优化。
这里引入一个比较最著名的------单调队列优化。其主要解决的状态转移方程形如:
\dp\[i = \min\limits_{j \in L(i), R(i)} \{ dpj + cost(i, j) \} + C(i) \]
使用单调队列的话,就可以将本质 \(O(n^2)\) 的算法优化到 \(O(n)\) (因为单调队列本质常数嘛)
但是如果这个cost不仅仅是相加了,而是相乘呢?比如这个式子形如:
\dp\[i = \min_{j} \left\{ dpj + a_i \cdot b_j + d_j \right\} + c_i \]
初看好像不会觉得有什么问题,但是关键的问题就是这个转移必须依赖 \(i\) 。这样原本现在看来比较劣的 \(j\)。在后面某一个 \(i\) 的作用下变得很优。这样你的单调队列就会直接伪了。
那这样我们就引入一个更加优秀的算法 ------ 斜率优化dp。
正文
貌似直接写的话有点抽象,所以引用一个具体的转移方程我们就可以大力推式子力。
我们依据题意写出来这样一个转移式子:
\dp\[i=\min_{1\le j <i}\left\{dpj+((i-j-1)+prei-prej-l)^2\right\} \]
Tips:\(dpi\) 表示把前 \(i\) 个玩具装进箱子里的最小花费。
直接 \(O(n^2)\) 暴力的话,量级高达2.5e9肯定是跑不完的。考虑优化。斜率优化最重要的就是爆拆式子,我们考虑把后面一坨移向得。
\((i+pre\[i-1-l)-(j+prej))^2 \]
但是这东西直接展开拆出来的向未免有点太多了,所以引入一个小trick------将前面一坨与 \(i\) 有关的存到数组 \(B\) 里,后面一坨与 \(j\) 有关的存到数组 \(A\) 里。带入然后展开我们的转移方程就会变为:
\dp\[i=\min_{1\le j <i}\left\{dpj+Aj^2-2\times Bi\times Aj+Bi^2\right\} \]
但是我们做了这么多有什么用呢?请欣赏本篇代码最精华的部分------斜率优化。(观前提示:如果实在没看懂也没关系,可以直接背板子,但我会努力讲清楚的)
结合引言,不能使用单调队列的主要矛盾就是 \(i\) 也参与了运算。那我们不妨假设有两个决策点 \(j_1<j_2\) ,哪个在 \(i\) 的限制下更优秀。
回忆小学时期学的一次函数,形如 \(y=kx+b\) 不妨假设 \(y\) 就是只与 \(j\) 有关的项:\(dpj+Aj^2\) 。按照这个形式带入可得出 \(k\) 即为 2\\times B\[i\] ,\(x\) 即为 \(Aj\),而 \(Bi^2\)为常数项 \(b\)。
如果我们对于每一个 \(j\) 把它看成平面上的一个点:
\(x_j,y_j)=(A\[j,dpj+Aj^2) \]
那么我们想要最小化的东西,即为:
\y_j-k \\times x_j \\
等价于:
\dp\[i=y_j-k \times x_j + b \]
继续回忆一次函数相关知识:简单解个方程可得,一条直线 \(y=kx+b\) 经过 \((x_j,y_j)\),则其 \(b\) 就为 \(y_j-k \times x_j\) ,然后你就会惊奇的发现这个 \(b\) ,竟然与我们要找到最小值一样! 那感情好啊,直接最小化这个 \(b\) 就好了。
因为要维护最小值,考虑维护一个下凸壳,使得决策点随着 \(k\) 的变化单调移动。
下凸壳的定义
下凸壳凸包的下边界,也就是从最左端的点到最右端的点,沿下走的那条折线。
更直观地:

为什么要维护下凸壳?考虑在平面上画一个斜率为 \(k\) 的直线,碰到的第一个点,即为 \(b\) 的最优解。而这个点一定在下凸壳上。需要注意的问题,我们不能直接抛弃掉 \(j\)。 还是同样的问题,一个在当前 \(i\) 比较劣的 \(j\) 在后面可能就会变得很优秀从而变成未来又可能最优的决策点。
对着这个定义,我们直接考虑之前学的单调队列来维护可能成为下凸壳的所有 \(j\)。
那么最后的问题,对于两个决策点谁是更优秀的 \(k\) ? 其他人可能讲的是列一些式子然后化简?但是貌似结合xxs都知道的简单知识 \(k=\frac{y_2 - y_1}{x_2 - x_1}\) 根据一次函数的定义秒掉这个问题?注意到由 \((x_1,y_1)\) 和 \((x_2,y_2)\) 组成的这条线斜率与 \(k\) 的大小关系决定谁更优。那么 \\frac{y_2 - y_1}{x_2 - x_1} \< k ,弹出队首。 \(\frac{y_2 - y_1}{x_2 - x_1} \ge \frac{y_2 - y_3}{x_2 - x_3}\),弹出队尾。这就是斜率优化最为精髓的地方。
还有吗,貌似没了?
代码实现
个人觉得代码是显然的,配合代码食用讲解效果更佳。
cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+10 ;
int dp[N],sum[N],que[N],a[N],b[N] ;
int head=1,tail=0 ;
// dp[i]=min(dp[j]+((i-j-1+sum[i]-sum[j]-l)^2))
// y=dp[j]+a[j]^2
// x=a[j]
// k=2*b[i]
int siz() {return tail-head+1 ; }
int fx(int i) {return a[i] ;}
int fy(int i) {return dp[i]+a[i]*a[i] ;}
int fk(int i) {return 2*b[i] ;}
double slope(int a,int b) {
int x1=fx(a),y1=fy(a),x2=fx(b),y2=fy(b) ;
return (x1==x2)?(y1-y2)*1.0/1e-9:(y1-y2)*1.0/(x1-x2) ;
}
signed main() {
ios::sync_with_stdio(0);cin.tie(0); cout.tie(0) ;
int n,l ; cin>>n>>l ;
for(int i=1,x;i<=n;i++) {cin>>x ; sum[i]=sum[i-1]+x ;}
for(int i=1;i<=n;i++) {a[i]=i+sum[i] ; b[i]=i+sum[i]-1-l ;}
for(int i=1;i<=n;i++) {
while(siz()>=2&&slope(i-1,que[tail])<=slope(que[tail],que[tail-1])) tail-- ;
que[++tail]=i-1 ;
while(siz()>=2&&slope(que[head+1],que[head])<=fk(i)) head++ ;
int j=que[head] ;
dp[i]=fy(j)-fk(i)*fx(j)+b[i]*b[i] ;
}
cout<<dp[n] ;
cout.flush() ;
return 0;
}
后记
请注意,这个方法有效的情况,当且仅当 \(k\) 递增。