斜率优化
可以优秀的解决一些最大值相关的DP
我们考虑一个问题
我们很简单可以求出 $O(n^2)$ 的巨简单的做法。
我们设 $f_i$ 表示
\[f_i=min_{j<i}\{f_j+(sum_i-sum_j+i-j-1-L)^2\}\]我们考虑用 $s_x$ 代替 $sum_x+x$, $L’$ 代替 $L+1$ ,并且把与 $j$ 无关的东西从右边拿到左边。得到:
\[f_i-(s_i-L')=min_{j<i}\{f_j+s_j^2+2s_j(L'-s_i)\}\]我们考虑用直线 $b=y-kx$ 的形式转换这个式子。
\[\begin{cases} x=s_j \\ y=f_j+s_j^2 \\ k=-2(L'-s_i) \\ b=f_i-(s_i-L)^2 \end{cases}\]一般来说,我们取的 $x,y$ 与 $j$ 有关, $k,b$ 与 $i$ 有关, 其中 $b$ 与 $f_i$ 有关,$y$ 与 $f_j$ 有关。
因此在进行这些转化后,我们可以把 $j$ 抽象成一个图上的点,所以我们把所有的决策都丢到了平面上。然后我们用斜率为 $k$ 的直线分别穿过这些点,此时直线的纵截距就反应了 $f_i$,因此我们需要使这个纵截距最小化。
因此我们可以在完成 $f_i$ 求值后将 $i$ 所对应的 $x,y$ 求出并放到图中维护一个下凸壳,每次用斜率为 $k$ 的直线去切这个下凸壳即可。
具体来说这个目标的直线的斜率是单调递增的,而且凸包也是单调递增的,这些我们都可以用单调队列维护。每次一直弹出斜率小于当前斜率的,弹完之后的就是当前的答案。之后弹出末尾的不优的,然后加入当前点。
需要注意的是,除了最开始放入的初始点外,一定要保证凸包中至少存在一个点。不然更新出来的东西是有问题的。
附上模板题代码
#include<bits/stdc++.h>
#define ll long long
#define db double
#define filein(a) freopen(#a".in","r",stdin)
#define fileot(a) freopen(#a".out","w",stdout)
#define sky fflush(stdout)
template<class T>
inline void read(T &s){
s=0;char ch=getchar();bool f=0;
while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=getchar();}
while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=getchar();}
if(ch=='.'){
db p=0.1;ch=getchar();
while('0'<=ch&&ch<='9') {s=s+(ch^48)*p;p*=0.1;ch=getchar();};
}
s=f?-s:s;
}
const int N=5e4+3;
int n,L;
int c[N];
ll sum[N],f[N];
inline ll calcx(int j){
return (sum[j]+j);
}
inline ll calcy(int j){
return f[j]+(sum[j]+j)*(sum[j]+j);
}
inline ll calck(int i){
return -2*(L+1-sum[i]-i);
}
inline db slope(int i,int j){
return (db)(calcy(j)-calcy(i) )/(db)(calcx(j)-calcx(i) );
}
int l,r;
int q[N];
int main(){
filein(a);fileot(a);
read(n);read(L);
for(int i=1;i<=n;++i){
read(c[i]);
sum[i]=sum[i-1]+c[i];
}
l=r=1;
for(int i=1;i<=n;++i){
while(l<r and slope(q[l],q[l+1])<=calck(i) ) ++l;
f[i]=calcy(q[l])-calck(i)*calcx(q[l])+(sum[i]+i-L-1)*(sum[i]+i-L-1);
while(l<r and slope(q[r-1],q[r])>=slope(q[r-1],i) ) --r;
q[++r]=i;
}
printf("%lld\n",f[n]);
return 0;
}
那么如果不保证每次加入直线的斜率单调变化呢?我们考虑下面的做法。
二分,CDQ,平衡树优化
我们如果考虑玩具装箱这个题的玩具大小可以为负,此时我们直线的斜率不再单调,而且每次加入的决策点的横坐标不再单调。
我们考虑把剔除队首换为在凸壳上二分找到斜率最接近的那个条凸壳上的边,然后可以得到最优决策点。
而当我们加入决策点时,我们有两种维护方法,先讲第一种。
我们直接考虑用平衡树维护凸壳,加入新的决策点就直接在平衡树上加入节点,寻找最优决策点的二分操作直接换成平衡树的查询操作即可。删除决策点时也是照样删除即可。
第二种方法是考虑CDQ分治。我们用CDQ分治的方法算出左半边的 $f_i$, 然后对于左半边的决策点建凸壳,以此更新右半边,我们对于右半边的点按照斜率排序,就可以单调队列维护了。不过由于需要排序,我们考虑直接二分也未曾不可。左边的决策点用完就可以扔掉了,于是我们先算完左区间,完成当前的统计后再扔掉所有决策点进入右半边即可。
这就是最基本的一些维护,具体情况我们还是开发思维自己再因题而异地自己想想吧!