斜率优化学习笔记
看到出现了可以写题解的 P10979 任务安排 2,于是写学习笔记了。
斜率优化模板题,有三倍经验,难度逐渐递增,建议从前做到后。
P2365 任务安排,P10979 任务安排 2,P5785 [SDOI2012] 任务安排。
(但是我这种做法 P10979 和 P5785 没有区别。
思路:
设
但是
观察到转移式的
动态规划的转移参照 P2365 任务安排的这篇题解。
下面讲一个不太常用的斜率优化方法。
我们先把转移式做一些变化:
即把常数项提到
上述转移式,时间复杂度瓶颈是找到最优决策点,所以我们考虑两个决策点
现在我们钦定
不等式的左边是不是很像斜率式?我们令
该不等式可以用文字描述为:若两点之间的斜率大于
我们现在考虑三个点的情况,若这三个点
- 直线
和直线 的斜率都大于 ,此时 点优于 点, 点优于 点, 点不是最优的,如图: - 直线
的斜率大于 ,直线 的斜率小于 ,此时 点优于 点, 点优于 点, 点不是最优的,请读者自行画图。 - 直线
和直线 的斜率都小于 ,此时 点优于 点, 点优于 点, 点不是最优的,请读者自行画图。
综上,若三个点围成一个上凸壳,那么中间的那个点一定不是最优的。
现在考虑转移
这个点形象化的来说就是用斜率为
其中 E 就是最优决策点。
然后考虑如何维护这个下凸壳,这个还是比较容易想到,假设在此之前已经维护好了之前所有点的下凸壳,需要加上该点。由于
这道题就做完了。
对于这种做法来说,P10979 任务安排 2 和 P5785 [SDOI2012] 任务安排没有区别,因为这两道题的
做完这道题,我们分析一下什么题可以用斜率优化,首先他要求出一段区间的某个函数值的极值,而这个函数值中包含
现在来讲为什么我们是要钦定
代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 3e5 + 5;
int n, s, t[kMaxN], c[kMaxN], top, L, R, M;
long long f[kMaxN];
struct P {
int x;
long long y;
} stk[kMaxN];
long double slope(P i, P j) { return i.x == j.x ? (i.y > j.y ? -4e18 : 4e18) : (long double)(i.y - j.y) / (i.x - j.x); }
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> s;
for (int i = 1; i <= n; i++) {
cin >> t[i] >> c[i], t[i] += t[i - 1], c[i] += c[i - 1];
}
for (int i = 1; i <= n; i++) {
for (; top > 1 && slope(stk[top - 1], stk[top]) > slope(stk[top], {c[i - 1], f[i - 1]}); top--) {
}
stk[++top] = {c[i - 1], f[i - 1]}, L = 1, R = top, M = L + R >> 1; // 加入点,维护凸壳
for (; L < R; M = L + R >> 1) {
(M == top ? 4e18 : slope(stk[M], stk[M + 1])) > s + t[i] ? R = M : L = M + 1;
} // 二分最优决策点
f[i] = stk[L].y - 1LL * (s + t[i]) * stk[L].x + 1LL * s * c[n] + 1LL * c[i] * t[i]; // 转移
}
cout << f[n];
return 0;
}
作为一篇斜率优化的学习笔记,还是讲一道点的横坐标不单调的例题吧(毕竟这种方法比较常见),例题:P4655 [CEOI2017] Building Bridges。
思路:
设
同样将式子转化一下:
若决策点
我们令
我们设点
我们先将所有的点按照横坐标排序,然后将所有的点分成两半(设中点为
简单讲一下这种做法的正确性,不需要用 cdq 分治的做法其实是用一个凸包来更新 dp 值,而用了 cdq 分治的做法其实是用
代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 1e5 + 5;
int n, h[kMaxN], top, L, R, M;
long long s[kMaxN], f[kMaxN];
struct P {
int i, x;
long long y;
} stk[kMaxN], a[kMaxN];
long double slope(P i, P j) { return i.x == j.x ? (i.y > j.y ? -4e18 : 4e18) : (long double)(i.y - j.y) / (i.x - j.x); }
void S(int l, int r) {
if (l == r) {
a[l].y = f[a[l].i] + 1LL * a[l].x * a[l].x - s[a[l].i];
return;
}
int mid = l + r >> 1;
stable_partition(a + l, a + r + 1, [&](P p) { return p.i <= mid; }); // 按照编号分成两半
S(l, mid), top = 0; // 算出前一半的 dp 值
for (int i = l; i <= mid; i++) {
for (; top > 1 && slope(stk[top - 1], stk[top]) >= slope(stk[top], a[i]); top--) {
}
stk[++top] = a[i];
} // 构成凸壳
for (int i = mid + 1; i <= r; i++) {
L = 1, R = top, M = L + R >> 1;
for (; L < R; M = L + R >> 1) {
(M == top ? 4e18 : slope(stk[M], stk[M + 1])) > 2 * h[a[i].i] ? R = M : L = M + 1;
}
f[a[i].i] = min(f[a[i].i], stk[L].y - 2LL * h[a[i].i] * stk[L].x + 1LL * h[a[i].i] * h[a[i].i] + s[a[i].i - 1]);
} // 转移
S(mid + 1, r); // 继续递归
inplace_merge(a + l, a + mid + 1, a + r + 1, [](P i, P j) { return i.x < j.x; }); // 合并
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> h[i];
}
memset(f, 0x3f, sizeof(f)), f[1] = 0;
for (int i = 1; i <= n; i++) {
cin >> s[i], s[i] += s[i - 1], a[i] = {i, h[i], 0};
}
sort(a + 1, a + 1 + n, [](P i, P j) { return i.x < j.x; });
S(1, n), cout << f[n];
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】