【学习笔记】浅谈斜率优化 dp
写一篇学习笔记,以防自己忘记。
引入
我们以 HDU 上的一道题为例。
Vjudge 提交入口
:::info[简易版题意]
现在有一个长度为
数据范围满足
暴力做法
显然的 dp,我们考虑这样设计状态:
答案就是
斜率优化
这个式子不是很好的优化啊!咋办?
先竭尽所能的化简:
注意到
我们印象中的一次函数长这样:
说明一下其中的对应关系:
注意到这里笔者特意将含
注意到这里
注意到我上面话的意思就是要让那条直线向上平移最短的距离,碰到的第一个点就是能够让
:::info[下凸壳的定义]
- 几何直观定义: 包围所有点的一条最底部的折线,使得没有任何一个点在这条折线的下方。
-
代数本质定义(写代码的核心): 从左到右,相邻两点连线的斜率严格单调递增(即
k_1 < k_2 < k_3 \dots )。一句话总结: 下凸壳就是斜率越来越大的底部折线。 :::
:::info[为什么最优的点一定出现在下凸壳上面]
假设有三个点
如果中间的
我们在求最优解时,相当于用一条斜率为
- 如果直线的斜率
k 较小(即k < k_{AB} ),直线在平移碰到B 之前,一定会先碰到左边的A 。 - 如果直线的斜率
k 较大(即k \ge k_{BC} ),直线在平移碰到B 之前,一定会先碰到右边的C 。
因为
因此,位于连线上方的
我们现在要维护这个下凸壳。注意到我们的
找到
这里其实还没结束。我们算出
我们现在完全盯着上面那个图:新加的 while 循环判断,如果破坏了结构就将队尾弹出,直到满足单调递增为止,然后再将
这一块确实有点抽象,建议没看懂的同学自己在脑海里面多过几遍。
代码特别好写了:
::::success[code]
:::warning[关于斜率的计算部分]{open}
如果我们现在要计算
所以我们在写题时如果意识到这道题目中的横坐标可能相同,那么记得对其作出处理!不然可能会喜提 RE。
:::
:::warning[一个小细节]{open}
注意到我的首先单调队列初始情况下
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=5e5+5;
int n,m,f[N],q[N],s[N],head,tail,x;
double slope(int x,int y,int xx,int yy){return x==xx?(yy>=y?1e18:-1e18):1.0*(yy-y)/(xx-x);}
int get_x(int x){return s[x];}
int get_y(int x){return f[x]+s[x]*s[x];}
signed main(){
while(cin>>n>>m){
head=tail=0;
for(int i = 1;i<=n;i++)
cin>>x,s[i]=s[i-1]+x;
for(int i = 1;i<=n;i++){
int k=2*s[i];
while(head<tail&&slope(get_x(q[head]),get_y(q[head]),get_x(q[head+1]),get_y(q[head+1]))<k) head++;
f[i]=f[q[head]]+(s[i]-s[q[head]])*(s[i]-s[q[head]])+m;
while(head<tail&&slope(get_x(q[tail-1]),get_y(q[tail-1]),get_x(q[tail]),get_y(q[tail]))>=slope(get_x(q[tail]),get_y(q[tail]),get_x(i),get_y(i))) tail--;
q[++tail]=i;
}
cout<<f[n]<<'\n';
}
return 0;
}
::::
时间复杂度显然是
然后我们发现斜率优化的大部分题就是列出式子,然后再转化为一次函数的形式,剩下的就是模版了。
拓展
P5785 [SDOI2012] 任务安排
我们先来思考一下这道题目怎么做。
首先先列出来暴力的转移式长什么样子(定义
这个式子就是先将这一次分组的
化简得:
移项得:
其中
接下来就是模版的事了。
::::success[code]
:::warning[该代码疑似有被 hack 的风险]{open}
注意到该代码中计算斜率的函数并没有判断
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=3e5+5;
int n,s,t[N],c[N],x[N],y[N],f[N],q[N],head,tail;
double slope(int x,int y,int xx,int yy){return 1.0*(yy-y)/(xx-x);}
signed main(){
cin>>n>>s;
for(int i = 1;i<=n;i++)
cin>>x[i]>>y[i],t[i]=t[i-1]+x[i],c[i]=c[i-1]+y[i];
for(int i = 1;i<=n;i++){
int k=s+t[i];
while(head<tail&&slope(c[q[head]],f[q[head]],c[q[head+1]],f[q[head+1]])<=k) head++;
f[i]=f[q[head]]+s*(c[n]-c[q[head]])+t[i]*(c[i]-c[q[head]]);
while(head<tail&&slope(c[q[tail-1]],f[q[tail-1]],c[q[tail]],f[q[tail]])>=slope(c[q[tail]],f[q[tail]],c[i],f[i])) tail--;
q[++tail]=i;
}
cout<<f[n];
return 0;
}
::::
好的现在我们再来看一下当时间不一定为正数咋做 (吐槽一下,时间为负数这合理吗)。时间不一定为正数这意味着我们的前缀数组
时间复杂度显然的,每一次都要一个二分,
:::success[code]
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=3e5+5;
int n,s,x,y,c[N],t[N],q[N],f[N],head,tail;
double slope(int x,int y,int xx,int yy){return x==xx?(yy>=y?1e18:-1e18):1.0*(yy-y)/(xx-x);}
signed main(){
cin>>n>>s;
for(int i = 1;i<=n;i++)
cin>>x>>y,t[i]=t[i-1]+x,c[i]=c[i-1]+y;
for(int i = 1;i<=n;i++){
int l=head,r=tail-1,res=tail,k=s+t[i];
while(l<=r){
int mid=l+r>>1;
if(slope(c[q[mid]],f[q[mid]],c[q[mid+1]],f[q[mid+1]])>k) r=mid-1,res=mid;
else l=mid+1;
}
f[i]=f[q[res]]+s*(c[n]-c[q[res]])+t[i]*(c[i]-c[q[res]]);
while(head<tail&&slope(c[q[tail-1]],f[q[tail-1]],c[q[tail]],f[q[tail]])>=slope(c[q[tail]],f[q[tail]],c[i],f[i])) tail--;
q[++tail]=i;
}
cout<<f[n];
return 0;
}
:::
其实还有一种被破坏单调性的可能,就是横坐标
撰写不易,能给个赞再走吗 qwq