斜率优化学习笔记
前置芝士:
- 一次函数(好吧其实你只要知道斜率)
- 基本的动态规划能力(暴力的转移)
- 一定的数学能力(指拆平方括号和合并同类项
Part.0 目录
- 铺垫
- 基本策略
- 什么题适合&需要斜率优化
- 基础习题讲解
- 拓展习题讲解
Part 1.铺垫
什么是斜率?
对函数有了解的人都知道,在一次函数中,对于平面直角坐标系的任意两点
Part.2 基本策略
斜率如何帮助我们对动态规划进行优化呢?
首先,对于一个任意裸的状态转移方程,一般来说我们需要循环枚举所有可能的决策点,然后维护答案,但是在一些情况下这么做太慢的,容易惨遭
以 HDU-3507 为例,状态转移方程不再赘述,假设
这个形式有没有想到什么?
这不就是斜率的标准形式吗?
那么,可以将目前可行的决策点丢进一个队列里,然后只要从队首开始,通过斜率判断此时的队头和队列第二项作为决策点是的斜率不等式是否成立,只要成立,那么 pop
掉,然后继续向上找的符合条件的决策点,反之,直接状态转移方程。
值得一提的是,很多 blog 会画图用上凸壳下凸壳解释,这样确实便于理解但是我这里为了思路简单明了(说白了就是不可能每道题去画图吧)直接从理论分析了 qwq
代码语言则为:while(l+1<=r&&top(q[l+1],q[l])<=2*sum[i]*down(q[l+1],q[l]))l++;
top
& down
分别指分子公式 & 分母公式。
因为要提取的不止队首,所以这个队列要手写(不过你 front
两次也没问题但是又不好写常数又大)。
然后,为了答案的最优性,队列中每相邻两点的对应斜率单调递增,不然反正之后也会被之前的步骤拜拜掉,所以,该队列实际上是一个单调队列。
这样处理之后,就可以排除很多的臃余状态,时间复杂度由
即:while(l+1<=r&&top(i,q[r])*down(q[r],q[r-1])<=top(q[r],q[r-1])*down(i,q[r]))r--;
然后将当前节点入队即可。
还有一个细节,为了 while
能顺利执行,我们要在动态规划之前加入一个虚拟节点即 q[++r]=0;
那么到这里,这一题基本结束,斜率优化的基本步骤也到此为止。
Part.3 什么题目应当使用斜率优化呢?
显然,为了能构成斜率,状态转移方程形式如下:
其中,
如果后面是仅与
Part.4 基础习题
1.HDU-3507
见上。
先推柿子。
基本状态转移方程:
仍然假设
接下来怎么办呢?爆展?
可以按照与
这个东西是可以预处理出来的呀!!1
设
接下来就清爽多了.
参考代码:
#include<bits/stdc++.h>
#define int long long//不开ll见祖宗
using namespace std;
int n,m,q[50005],sum[50005],dp[50005],h[50005],g[50005];
int top(int i,int j){//斜率分子
return (dp[i]+g[i]*g[i])-(dp[j]+g[j]*g[j]);
}
int down(int i,int j){//斜率分母
return g[i]-g[j];
}
signed main(){
cin>>n>>m;{
for(int i=1;i<=n;i++)cin>>sum[i];
sum[0]=dp[0]=0;
for(int i=1;i<=n;i++){
sum[i]+=sum[i-1];
h[i]=sum[i]+i-1-m;
g[i]=sum[i]+i;//预处理
}
int l=1,r=0;
q[++r]=0;
for(int i=1;i<=n;i++){
while(l+1<=r&&top(q[l+1],q[l])<=2*h[i]*down(q[l+1],q[l]))l++;
int j=q[l];
int tmp=i-j-1+sum[i]-sum[j]-m;
dp[i]=dp[j]+tmp*tmp;
while(l+1<=r&&top(i,q[r])*down(q[r],q[r-1])<=top(q[r],q[r-1])*down(i,q[r]))r--;//如上
q[++r]=i;
}
cout<<dp[n]<<'\n';
}
return 0;
}
基础状态转移方程:dp[i]=dp[j]+a*(sum[i]-sum[j])*(sum[i]-sum[j])+b*(sum[i]-sum[j])+c;
开 始 愉 (lie) 快 (kai) 地 推 式 子:
经过爆展和一系列合并之后如下:
然后套用之前的模板(这次求最大值!不要弄反了!)
#include<bits/stdc++.h>
#define int long long//不开ll见祖宗
using namespace std;
int n,m,q[1000005],sum[1000005],dp[1000005],a,b,c;
int top(int i,int j){
return (dp[i]+a*sum[i]*sum[i]-b*sum[i])-(dp[j]+a*sum[j]*sum[j]-b*sum[j]);
}
int down(int i,int j){
return sum[i]-sum[j];
}
signed main(){
cin>>n>>a>>b>>c;
for(int i=1;i<=n;i++)cin>>sum[i];
sum[0]=dp[0]=0;
for(int i=1;i<=n;i++){
sum[i]+=sum[i-1];
}
int l=1,r=0;
q[++r]=0;
for(int i=1;i<=n;i++){
while(l+1<=r&&top(q[l+1],q[l])>=2*a*sum[i]*down(q[l+1],q[l]))l++;
int j=q[l];
dp[i]=dp[j]+a*(sum[i]-sum[j])*(sum[i]-sum[j])+b*(sum[i]-sum[j])+c;
while(l+1<=r&&top(i,q[r])*down(q[r],q[r-1])>=top(q[r],q[r-1])*down(i,q[r]))r--;
q[++r]=i;
}
cout<<dp[n]<<'\n';
return 0;
}
跟之前代码差别不大,重在柿子。
Part.5 拓展习题讲解
之前都是简单的
那么如果出现了更加复杂的情况呢?
例如 P2365 任务安排
高能预警
考虑费用提前,先摆上方程:
//待填坑,关于费用提前的说明
有人会问:这么写
这就是费用提前法的高明之处了:
为什么呢?
请看到
其实到这一步,因为 (卡最优解,我们还是要推式子。
没有平方,应该还是好推的。
自行参考如下。
int top(register int i,register int j){
return (dp[i]-s*sf[i])-(dp[j]-s*sf[j]);
}
int down(register int i,register int j){
return sf[i]-sf[j];
}
接下来又是板子,由此可见当斜率优化加上各种奇怪优化等于毒瘤(划掉。
最后一个习题:P3648
作为 APIO 的原题,质量还是很高的。
基本状态转移方程:
f[i]=dp[j]+sum[j]*(sum[i]-sum[j]);
本来是二维,但是通过滚动数组的方式可以降维优化。
接下来,只要进行
因为是最后一道题,柿子和代码咕咕咕掉了((,有问题可以康康题解。
完结撒花!!1
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现