DP专题-学习笔记:斜率优化 DP
一些 update
update 2021/4/19:最近在洛谷讨论区的学术版里面看到一篇帖子,是关于斜率相同时是否需要弹出队列的问题,笔者在看完这篇帖子之后,发现这个细节是很重要的,故加上。
update 2022/1/8:修改了一个地方的语言,不影响本篇文章的理解,但是对于一些斜率优化的题目还是有影响的。
1. 前言
斜率优化 DP,是一种优化动态规划的方式,采用线性规划的方式来优化动态规划。
斜率优化 DP 一般是拿来优化 1D/1D 的动态规划的。
什么是 1D/1D 的动态规划?
1D/1D 的动态规划就是指状态是一维,转移也是一维的动态规划。
式子一般长这样:,其中 是关于 的一个有特殊条件的式子。
代码一般长这样:
for (int i = 1; i <= n; ++i)
for (int j = 0; j < i; ++j)
f[i] = Min/Max(f[i], 某个跟 j 有关的特殊式子);
那么这个特殊条件是什么呢?
这种 DP 的转移方程分 2 类:
- 形如 的,其中 是一个只关于 的式子, 是一个只关于 的式子。
像这样的 DP,一般是可以采用单调队列/单调栈/数据结构维护的。 - 形如 的,其中 同上, 是一个存在形如 的项的式子,比如说 。
这个时候,如果你将这个式子拆开就会发现,里面存在一个项 ,此时普通的单调队列就不能解决了,需要采用斜率优化。
斜率优化是基于单调队列实现的,因此请先学习单调队列。
注意本文所讨论的所有题目默认求的斜率单调递增,如果不是单调递增请采用李超线段树解决。
李超线段树实际上是更加一般的做法,复杂度是 ( 是值域注意不直接是 值域),详情请见:数据结构专题-学习笔记:李超线段树
2. 详解
2.1 暴力方程
斜率优化的第一步是设计出暴力方程而且要是 1D/1D 的。
设 表示从 1 到 分段之后的最大战斗力, 为 的前缀和数组,那么就有状态转移方程:
初值为 ,其余为 。
这个方程应该还是很好理解的吧~
暴力得分 50pts。
我第一次写暴力的时候出了点意外得 40pts
2.2 线性规划
接下来考虑优化。
斜率优化,肯定跟斜率有关。
而斜率优化的关键步骤如下:
- 拆式子。
- 化成形如 的式子。
- 根据求 的 还是 决定维护上凸壳还是下凸壳。
什么意思呢?
考虑拆式子,拆成 ,其中 与 有关, 与 有关,所有常数项放到 中。
先将原始方程拆掉:
拆掉了之后,根据上述描述,转变式子:
此时此刻 。
那么考虑 的几何意义,那么就可以看成过点 的直线,其中斜率 ,我们的任务是让截距 最大化。
考虑下面这张图:
显然,对于相同的斜率 ,过 D 点的截距肯定比过 C 点的截距要小。
这个可以利用线性规划理解。
因此基本思路就出来了:要使截距最大化,肯定要使斜率逐渐变小(结合上图理解);要使截距最小化,肯定要使斜率逐渐变大。
而对于前面的几个点,如果其斜率大于了 ,那么显然当前点比直接从 点分割更劣,直接删掉即可。
对于后面的几个点,采用类似单调队列的方式保证斜率单调即可。
从图像上,使截距最大化就是使斜率递减,也就是维护上凸壳;使截距最小化就是使斜率递增,也就是维护下凸壳。
那么如何记转换式子呢?, 与 有关, 与 有关,这样记就好了。
因此,对于上面的方程,采用单调队列维护一下斜率,然后每一次取出队尾(因为此时队尾一定是最优的),这样就可以做到 解决。
关于队首与队尾的斜率相同时是否需要弹出队列的问题:
假设当前的点是这样的:
从图上可以清晰的看出来,对于 A,B,C 这三个点,其斜率全部都是相同的,而且前缀和也没有变化,貌似是可以弹出,但是看一下这条直线在这三个点上的截距 OD,OE,OF,会发现截距大小是不同的,而截距不同也会代表着 不同,如果选择弹出,程序可能会选择 AD 这条线,但是正确的答案是 CF 这条线。
2.3 代码
这里给一个保证写斜率优化代码正确的小技巧:
首先写暴力方程,然后写暴力程序,确定没问题就可以直接推式子斜率优化,最后两者对拍。
当然如果暴力方程写对了一般斜率优化是不会出问题的。
代码如下:
/*
========= Plozia =========
Author:Plozia
Problem:P3628 [APIO2010]特别行动队
Date:2021/4/15
========= Plozia =========
*/
#include <bits/stdc++.h>
typedef long long LL;
const int MAXN = 1e6 + 10;
int n, l, r, q[MAXN];
LL a, b, c, x[MAXN], f[MAXN];
LL read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return sum * fh;
}
LL Min(LL fir, LL sec) { return (fir < sec) ? fir : sec; }
LL Max(LL fir, LL sec) { return (fir > sec) ? fir : sec; }
double K(int i) { return 2.0 * a * x[i]; }
double X(int i) { return 1.0 * x[i]; }
double Y(int i) { return 1.0 * f[i] + a * x[i] * x[i] - b * x[i]; }
double Slope(int x, int y) { return (Y(x) - Y(y)) / (X(x) - X(y)); }
int main()
{
n = read(), a = read(), b = read(), c = read();
memset(f, -0x3f, sizeof(f)); f[0] = 0;
for (int i = 1; i <= n; ++i) x[i] = read() + x[i - 1];
q[l = r = 1] = 0;
for (int i = 1; i <= n; ++i)
{
while (l < r && Slope(q[l], q[l + 1]) >= K(i)) ++l;//队头删除
int j = q[l]; f[i] = f[j] + a * (x[i] - x[j]) * (x[i] - x[j]) + b * (x[i] - x[j]) + c;//转移
while (l < r && Slope(q[r], q[r - 1]) <= Slope(q[r], i)) --r;//队尾删除
q[++r] = i;
}
// for (int i = 1; i <= n; ++i)
// for (int j = 0; j < i; ++j)
// f[i] = Max(f[i], f[j] + a * (x[i] - x[j]) * (x[i] - x[j]) + b * (x[i] - x[j]) + c);
printf("%lld\n", f[n]); return 0;
}
3. 总结
-
使截距最大化维护上凸壳,使截距最小化维护下凸壳。
-
将方程转变为 , 与 有关, 与 有关。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具