斜率优化入门
斜率优化入门
对于两个\(dp[i]\)的决策点\(j_1,j_2\),满足当时\(\frac{Y(j_2)-Y(j_1)}{X(j_2)-X(j_1)}\leq S_ij_2\)比\(j_1\)更优,则可以维护下凸壳来去除一些一定不优的决策点
可以证明,当\(\frac{Y(j_2)-Y(j_1)}{X(j_2)-X(j_1)}\leq S_i\)时 \(j_2\) 比 \(j_1\) 更优,则上凸壳一定不优,维护下凸壳
反之,当\(\frac{Y(j_2)-Y(j_1)}{X(j_2)-X(j_1)}\geq S_i\)时 \(j_2\) 比 \(j_1\) 更优则维护上凸壳
然后当\(S_i\)不具有单调性的时候可以二分出最优的斜率
当\(S_i\)具有单调性时有决策单调性,用单调队列维护答案
看某位大佬博客看到的总结:
-
写出 \(dp\) 方程后,要先判断能不能使用斜优,即是否存在 \(function(i)∗function(j)\)的项。
-
通过大小于符号或者 \(b\) 中 \(dp[i]\) 的符号结合题目要求 \((min/max)\) 判断是上凸包还是下凸包,否则死活求不出答案。
-
(单调性出锅 1 号)将方程变为\(\frac{Y(j_2)-Y(j_1)}{X(j_2)-X(j_1)}\leq S_i\)或者 \(\frac{Y(j_2)-Y(j_1)}{X(j_2)-X(j_1)}\geq S_i\) 或者 \(kx+b=y\)的形式,变化要遵循之前提到的原则,尤其是 \(X\) 表达式的单调性,结合图形会更好理解。如果 \(X\) 不单调,那么需要用到 CDQ 分治或者平衡树维护凸包。
-
(单调性出锅 2 号)注意是否具有决策单调性,有时候打表只能得到片面的情况。当斜率不是单调递增时该怎么办?由于我们不知道什么时候会在什么地方取得最优决策点,所以必须要保留整个凸包以确保决策有完整的选择空间,查找答案就只能二分了,而不能直接取队首,比如这道题 [任务安排 33 Loj10186 BZOJ2726就不满足,其证明放在后面。
-
(单调性出锅 3 号)当 \(X\) 非严格递增时,那么在求斜率时可能会出现 \(X(j_1)==X(j_2)\) 的情况,最好是写成这样的形式:return \(Y(j)⩾Y(i)?inf:−inf\),而不要直接 return inf 或者 −inf,在某些题中情况较复杂,如果不小心画错了图,返回了一个错误的极值就完了,而且这种错误只用简单数据还很难查出来。
-
队列初始化要塞入一个点 \(P(0)\),还是以 玩具装箱 toy[P3195] 为例,塞入 \(P(S[0],dp[0]+(S[0]+L)^2)\)即 \(P(0,0)\),其代表的决策点为 \(0\)。
-
手写队列初始化是 \(h=1,t=0\) 由于塞了初始点导致 \(t\) 加 \(1\)
-
手写队列判断是否为空是 \({h⩽t}\),而出入队判断时都需要有至少 \(2\) 两个元素才能进行操作。所以应是 \({h<t}\) 。
-
计算斜率可能会因为向下取整而出现误差,所以 \(slope\) 函数最好设为 \(long\) \(double\)
-
有可能会有一部分的 \(dp\) 初始值无法转移过来,需要手动提前弄一下,例如 摆渡车 [P5017]
-
在比较两个斜率时,尽量写上等于,即 \(“⩽”,“⩾”\) 而不是 \(“<”,“>”\)。这样写对于去重有奇效(有重点时会导致斜率分母出锅),但不要以为这样就可以完全去重,因为要考虑的情况非常复杂,所以还是应该加上 5 中提到的特判,万无一失。
-
判断斜率大小时尽量把除转换成乘,防止爆精度
BOSS题
这题插入的点不具有单调性,需要用平衡树维护,非常🤢
splay的码可能过一段时间就不知道怎么搞了,准备省选前复习此题的cdq做法
/*
writer:TLE_AUTUMATIC 2019/10/15 8:30:25
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,s;
const int N = 300001;
inline int read(){
int x;scanf("%d",&x);return x;
}
const double eps=1e-15; //精度,尽量开小 ,但也不要太小,一般 -12 ~ -15
const double inf = 19260817019219.1;
namespace SPLAY{
int cnt,fa[N],ch[N][2];double x[N],y[N],lk[N],rk[N];int rt;
inline int get(int x){
return ch[fa[x]][1]==x;
}
inline void rotate(int x){
int f=fa[x],g=fa[f];
int kx=get(x),kf=get(f);
ch[f][kx]=ch[x][kx^1] , fa[ch[x][kx^1]]=f;
ch[x][kx^1]=f , fa[f]=x;
fa[x]=g;
if(g) ch[g][kf]=x;
}
inline void splay(int x,int &v){
int f,to=fa[v];
while(fa[x]!=to){
f=fa[x];
if(fa[f]!=to){
rotate(get(x)==get(f)?f:x);
}
rotate(x);
}
v=x;
} //以上是基础操作,但是要注意此处splay是把x变到v的位置
inline double fb(double now){
return now>0?now:-now;
}//fabs
inline double slope(int i,int j){
return (fb(x[i]-x[j])<=eps?-inf:(y[i]-y[j])/(x[i]-x[j]));
}//两点斜率
inline void insert(int &now,double nx,double ny,int lst){
if(!now){
now=++cnt;x[now]=nx,y[now]=ny,fa[now]=lst;return;
}
insert(ch[now][nx-x[now]>eps],nx,ny,now);
}//按x的大小插入
inline int pre(int now){
int u=ch[now][0],res=0;
while(u){
if(slope(now,u)>=rk[u]-eps) res=u,u=ch[u][0]; //斜率按中序遍历单调递减,此时继续往前走
else u=ch[u][1];
}
return res;
}//前驱
inline int nex(int now){
int u=ch[now][1],res=0;
while(u){
if(slope(now,u)<=lk[u]+eps) res=u,u=ch[u][1];
else u=ch[u][0];
}
return res;
}//后继
inline int getl(int now){
while(ch[now][0]) now=ch[now][0];
return now;
}//最左边
inline int getr(int now){
while(ch[now][1]) now=ch[now][1];
return now;
}//最右
inline void del(int x){
if(!ch[x][0]){
int r=getl(ch[x][1]);
splay(r,ch[x][1]),rt=r;//此时r的右子树是整棵树,因为没有比r更小的了(除了x)
ch[x][1]=fa[rt]=0;//删除整棵树
lk[rt]=inf;
}else if(!ch[x][1]){
int l=getr(ch[x][0]); //原理同上
splay(l,ch[x][0]),rt=l;
ch[x][0]=fa[rt]=0;
rk[rt]=-inf;
}else{
int rmin=getl(ch[x][1]),lmax=getr(ch[x][0]);
splay(lmax,ch[x][0]);
splay(rmin,ch[x][1]);
rt=lmax;ch[rt][1]=rmin;fa[rmin]=rt;
rk[rt]=lk[rmin]=slope(rt,rmin);//只保留两个点
}
}
inline void work(int x){
splay(x,rt);
if(ch[x][0]){
int l=pre(x);
if(l){
splay(l,ch[x][0]);
ch[l][1]=fa[ch[l][1]]=0;//删除l的子树
rk[l]=lk[x]=slope(l,x);
}
else lk[x]=-inf;
}else lk[x]=inf;
if(ch[x][1]){
int r=nex(x);
if(r){
splay(r,ch[x][1]);
ch[r][0]=fa[ch[r][0]]=0;//删除r的子树
lk[r]=rk[x]=slope(r,x);
}
else rk[x]=inf;
}else rk[x]=-inf;
if(lk[x]-rk[x]<=eps) del(x);//删除x
}
inline int get_ans(int x,double k){
if(!x) return 0;
if(lk[x]+eps>=k&&rk[x]<=k+eps) return x; //找到切点
if(lk[x]-eps<k) return get_ans(ch[x][0],k); //k>斜率,在左边,斜率单增
else return get_ans(ch[x][1],k);//k<斜率,在右边,斜率单减
}
};
double f[N],A[N],B[N],R[N];
int main(){
n=read(),scanf("%lf",&f[0]);
for(int i=1;i<=n;i++){
scanf("%lf%lf%lf",&A[i],&B[i],&R[i]);
int res=SPLAY::get_ans(SPLAY::rt,(-A[i]/B[i]));
double nx=SPLAY::x[res],ny=SPLAY::y[res];
f[i]=max(f[i-1],A[i]*nx+B[i]*ny);
ny=f[i]/(A[i]*R[i]+B[i]);
nx=ny*R[i];
SPLAY::insert(SPLAY::rt,nx,ny,0);
SPLAY::work(i);
}
printf("%.3lf",f[n]);
return 0;
}