算法学习笔记(2):与斜率优化共舞
斜率优化 DP
对于像如下这样的 dp 方程,我们可以使用斜率优化解决。
显然,如果
考虑将所有土地按
注意到此时被保留下的土地,
令
上面的方程复杂度为
先将
将所有能够转移到
- 有一条斜率为
的直线,这条直线需要经过上述的这些点中的一个或多个,并且希望它的截距最小。
上面这句话有点抽象,不妨将 原方程 与 一次函数 做一个对比,发现它们确实十分相似:
本质就是我们拿着一条斜率为
仔细观察可以发现,无论斜率 (
不妨先来维护这个凸壳,假设现在加入点
点可以直接加入凸壳。
记点
点加入后无法形成凸壳。
这时候一定有
由于这些点的
接下来就是要在维护出的凸壳上找到最优决策点,随便画一张图,由于这些直线的斜率一定不断增加,所以最优决策点一定不断向凸壳上方移动:
进一步观察,可以发现,如果凸壳上相邻的两点
时间复杂度
#include <bits/stdc++.h> using namespace std; int n, pos, q[50005], h, t; long long dp[50005]; struct land { int w, l; bool operator<(const land t) const { return w == t.w ? l > t.l : w > t.w; } } p[50005]; double X(int j) { return -p[j + 1].w; } double Y(int j) { return dp[j]; } double slope(int x, int y) { return (Y(x) - Y(y)) / (X(x) - X(y)); } int main() { scanf("%d", &n); for (int i = 1; i <= n; i++) scanf("%d%d", &p[i].w, &p[i].l); sort(p + 1, p + n + 1); for (int i = 1; i <= n; i++) if (p[i].l > p[pos].l) p[++pos] = p[i]; for (int i = 1; i <= pos; i++) { while (h < t && slope(q[h], q[h + 1]) <= p[i].l) h++; dp[i] = dp[q[h]] + p[q[h] + 1].w * 1LL * p[i].l; while (h < t && slope(q[t - 1], q[t]) >= slope(q[t], i)) t--; q[++t] = i; } printf("%lld", dp[pos]); return 0; }
例题:P3195 玩具装箱
记
令
即
将
化为直线的形式
类似与之前的形式,把每个
#include <bits/stdc++.h> using namespace std; typedef long long ll; int n, L, C, h, t, q[50005]; ll s[50005], dp[50005]; double X(int j) { return s[j] + j; } double Y(int j) { return (s[j] + j) * (s[j] + j) + dp[j]; } double slope(int i, int j) { return (Y(i) - Y(j)) / (X(i) - X(j)); } int main() { scanf("%d%d", &n, &L); for (int i = 1; i <= n; i++) { scanf("%d", &C); s[i] = s[i - 1] + C; } h = t = 1; for (int i = 1; i <= n; i++) { while (h < t && slope(q[h], q[h + 1]) <= 2 * (s[i] + i - L - 1)) h++; dp[i] = dp[q[h]] + sq(s[i] - s[q[h]] + i - q[h] - 1 - L); while (h < t && slope(q[t - 1], q[t]) >= slope(q[t], i)) t--; q[++t] = i; } printf("%lld", dp[n]); return 0; }
例题:P5785 任务安排(弱化版 P2365 任务安排)
至于方程是如何提前计算贡献的我就不细说了,记
展开整理成直线形式,即
每个
正准备敲板子的你突然发现了这一条限制:
也就是说
那么
可凸壳上相邻两点地斜率还是单调的啊!那么只需要在凸壳上二分,找决策点即可。
时间复杂度
#include <bits/stdc++.h> using namespace std; long long n, s, sumT[300005], sumC[300005], dp[300005], top, stk[300005]; long long X(int j) { return sumC[j]; } long long long Y(int j) { return dp[j]; } int main() { scanf("%lld%lld", &n, &s); for (int i = 1; i <= n; i++) { long long t, f; scanf("%lld%lld", &t, &f); sumT[i] = sumT[i - 1] + t; sumC[i] = sumC[i - 1] + f; } stk[++top] = 0; for (int i = 1; i <= n; i++) { int l = 1, r = top - 1, pos = stk[top]; while (l <= r) { int mid = (l + r) >> 1; if (Y(stk[mid + 1]) - Y(stk[mid]) > (sumT[i] + s) * (X(stk[mid + 1]) - X(stk[mid]))) pos = stk[mid], r = mid - 1; else l = mid + 1; } dp[i] = dp[pos] + sumT[i] * (sumC[i] - sumC[pos]) + s * (sumC[n] - sumC[pos]); while (top > 1 && (Y(stk[top]) - Y(stk[top - 1])) * (X(i) - X(stk[top])) >= (Y(i) - Y(stk[top])) * (X(stk[top]) - X(stk[top - 1]))) top--; stk[++top] = i; } printf("%lld", dp[n]); return 0; }
注:斜率优化比较时建议移项,把除法化为乘法;如果使用 double 类型的 slope 很可能会产生精度误差(本题就卡了。
斜率优化小结:
-
列出方程,先通过一些简单的代换化简成直线形式。
-
通过
以及 和 坐标的单调性判断凸壳方向。 -
如果直线斜率不单调,使用二分维护。
-
如果
坐标不单调,可以证明对最终凸壳没有影响。 -
如果
坐标不单调,可以考虑 分治解决,时间复杂度 。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具