学习笔记:斜率优化 DP
斜率优化
引入
首先给出一种更加简单的优化。考虑一个这样的的式子: 不难看出,这个式子中的每一项只会与 , 中的一个有关。显然可以转化为: 具体地,,我们可以考虑在转移的同时维护一个值 。对于每一次转移,我们先令 ,再令 ,这样就实现了 的转移。
然而,对于以下这种含有同时与 , 有关的项的式子,这种优化就显得有些力不从心了: 在这种情况下,我们就需要考虑更优的做法了。
实现
首先搬一道例题。
形式化题意:给定一个长度为 的序列 ,将这个序列分成若干块。
具体地,每一块的权值为 ,其中 ,, 是给定的系数。
对于 ,我们定义当前块的区间为 ,则 。
试找出一种方案使得 最大,并求出这个最大值。
我们可以很快地写出一个状态转移方程: 然而这个式子暴力转移的时间复杂度为 ,稳稳 TLE。
考虑斜率优化。首先浅浅推导一下: 显然某些项只与 有关,我们将它们都提出来: 对于一些只与 有关的项,显然无论如何这些项都不会随 的变化而变化。
我们令 ,
则原式化为 ,
再令 ,
则原式化为 ,
再分别令 ,,,。则原式化为: 显然这就是直线的斜截式方程。
考虑如何将 值(即 )最大化。
如果 且 不比 优,当且仅当 ,
移项得 ,
由于序列中的每个数都是非负数,所以 ,即 ,
不等式左右两边同时除以 得: 即 由 得: 不等式左右两边同时乘 得: 可以发现,不等式右边是一个斜率的表达式。更具体地,假设在平面直角坐标系上有两个点 ,,那么 此时就等价于过 , 两点的直线的斜率。此时可以采用斜率优化。
首先提炼一下之前推式子得到的结论:如果两个点相连所成的直线的斜率不大于 ,那么前面的点所对应的 就必然不是最优决策点;反之,后面的点所对应的 就必然不是最优决策点。
先来考虑一下 个点的简化情况:如下图所示,显然有 。
图中的每一个点都对应着一个 。
可以发现,无论如何 都不会成为最优决策点。
证明:
- 时, 才有可能成为最优决策点,此时有 。
- 时, 才有可能成为最优决策点,此时有 。
又 ,所以不存在任意一个实数 使得 且 。
综上所述, 无论如何都不能成为最优决策点。证毕。
所以这个点已经寄了,我们可以直接将它删去。
我们已经完美解决了 个点的情况,现在让我们试着将结论推广到题目的一般情况。
可以发现,如果存在三个横坐标递增的点,满足前两个点的斜率大于等于后两个点的斜率,那么就可以删去中间的那个点。
所以,如果我们处理出一个不可删点的点集的斜率数组(每相邻两个数的斜率),那么这个数组必然是递增的。
不难发现,在一个不可删点的点集中,最优决策点 满足:
- 点 与点 相连所成直线的斜率不大于 。
- 点 与点 相连所成直线的斜率大于 。
不难看出,这些点所成直线的斜率具有单调性,考虑通过二分答案来寻找最优决策点。
具体地,我们枚举 ,维护不可删点点集 。设 的长度为 ,两个点 , 相连所成直线的斜率为 。
- 二分答案找到最优决策点 。
- 执行状态转移。
- 不停弹出队尾直到:,这里的 指的是 的对应点。
- 将 加入 中。
总的时间复杂度为 。
斜率优化的精髓在于不可删点点集的单调性。正是因为这个单调性,我们才能采用二分去快速寻找最优决策点,从而将时间复杂度优化为 。
对于一些特定的题目,我们可以去掉 。如对于本题(特别行动队)而言, 是有单调性的;所以,每次我们可以不停地删去队头,直到队头满足最优决策点的性质;删完之后队头就是最优决策点。此时,每个元素至多入队一次出队一次,时间复杂度 。
现在来看看这道题的代码。
斜率优化的代码历来非常短,甚至连1KB都没有,但是细节非常多。斜率优化的注意事项非常重要,这里有必要阐述一下:
- 斜率优化可能会爆精度,比较两个斜率的时候可以交叉相乘。
- 如果不可删点点集的大小是 就不用再删点了。
- 在开始转移之前不可删点点集中有且仅有一个数 。
- 输出不要用 double,不然会自动转化为科学计数法的形式。
- 计算斜率必须 long double,除非采用交叉相乘法。
- 只有 具有单调性时才能用队列,否则只能 二分答案。
通常来说 的暴力转移都不难,可以考虑用对拍来查错(但是笔者现在忘得差不多了,这里提一嘴,先鸽着)。
#include <iostream>
#define int long long
#define double long double
#define MAXN 1000005
using namespace std;
int n, a, b, c;
int x[MAXN], f[MAXN], g[MAXN];
int q[MAXN], head = 1, tail = 1;
int read(){
int t = 1, x = 0;char ch = getchar();
while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
return x * t;
}
double getx(int x){return g[x];}
double gety(int x){return a * g[x] * g[x] - b * g[x] + f[x];}
double getk(int x, int y){return (gety(y) - gety(x))/(getx(y) - getx(x));}
signed main(){
n = read();a = read();b = read();c = read();
for(int i = 1 ; i <= n ; i ++)x[i] = read();
for(int i = 1 ; i <= n ; i ++)g[i] = g[i - 1] + x[i];
for(int i = 1 ; i <= n ; i ++){
while(head < tail && (a << 1) * g[i] <= getk(q[head], q[head + 1]))head++;
int j = q[head];
f[i] = f[j] + a * (g[i] - g[j]) * (g[i] - g[j]) + b * (g[i] - g[j]) + c;
while(head < tail && getk(q[tail - 1], q[tail]) <= getk(q[tail], i))tail--;
q[++tail] = i;
}
cout << f[n] << endl;return 0;
}
一些练习
Luogu P5785,Luogu P3195
这里顺便给出 Luogu P3195 的代码:
#include <iostream>
#define int long long
#define double long double
#define MAXN 50005
using namespace std;
int n, l, c[MAXN];
double f[MAXN], g[MAXN];
int q[MAXN], head = 1, tail = 1;
int read(){
int t = 1, x = 0;char ch = getchar();
while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
return x * t;
}
double geta(int x){return g[x] + x;}
double getb(int x){return geta(x) + l + 1;}
double getx(int x){return getb(x);}
double gety(int x){return f[x] + getb(x) * getb(x);}
double getk(int x, int y){return (gety(x) - gety(y))/(getx(x) - getx(y));}
signed main(){
n = read();l = read();
for(int i = 1 ; i <= n ; i ++)c[i] = read();
for(int i = 1 ; i <= n ; i ++)g[i] = g[i - 1] + c[i];
for(int i = 1 ; i <= n ; i ++){
while(head < tail && geta(i) * 2 > getk(q[head], q[head + 1]))head++;
f[i] = f[q[head]] + (geta(i) - getb(q[head])) * (geta(i) - getb(q[head]));
while(head < tail && getk(i, q[tail - 1]) < getk(q[tail - 1], q[tail]))tail--;
q[++tail] = i;
}
cout << (long long)f[n] << endl;return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】