动态规划的几种优化方式 Day2 斜率优化
前情提要:Day1 单调队列优化
斜率优化
引 洛谷 P2365 任务安排
题目地址:P2365
题目难度:提高+/省选-
个任务排成一个序列在一台机器上等待完成(顺序不得改变),这 个任务被分成若干批,每批包含相邻的若干任务。 从零时刻开始,这些任务被分批加工,第
个任务单独完成所需的时间为 。在每批任务开始前,机器需要启动时间 ,而完成这批任务所需的时间是各个任务需要时间的总和(同一批任务将在同一时刻完成)。 每个任务的费用是它的完成时刻乘以一个费用系数
。请确定一个分组方案,使得总费用最小。 输入格式:
第一行一个正整数
。 第二行是一个整数 。 下面
行每行有一对数,分别为 和 ,表示第 个任务单独完成所需的时间是 及其费用系数 。 输出格式:
一个数,最小的总费用。
数据范围:
对于
的数据, , , 。

按照样例最优解给出的图示如上。
根据题目,我们的总花费应是每一组所包含线段的
我们可以发现,最终结果受分组情况的影响。在每一组的开头,均有一段机器的启动时间,它可能会对后期某一组的结束时刻造成影响。有一个技巧,即提前把启动段造成的影响计算出来,而这很明显会牵扯到前缀和的计算。例如下边这个更为普遍的图:

假设 dp[i] 代表将前 dp[j] 可以直接拿来调用;对于剩下的待处理区间,我们的总花费是
注意到 dp[i] = min(dp[i], dp[j] + sumT[i] * (sumF[i] - sumF[j]) + s * (sumF[n] - sumF[j]))。
AcWing 301 任务安排2
题目地址:AcWing 301
题目难度:中等
题面相同,本题数据范围扩大到:
状态转移方程与上一题完全相同,我们在这里探讨它的优化方式。
我们的目的是把求解状态的时间复杂度降落到 dp[j] 和 sumF[j]。我们将
我们需要最小化

可以发现,无论直线的斜率再怎么变动,能让截距取得最值的那个点从始至终都是下边的那几个。事实上,对于斜率为
二维凸包是指内角均在 upper_bound 或者是二分解决。
在本题中,有一些奇妙的性质。这些性质允许我们不使用二分,而是直接
均为正整数,对于它们的前缀和显然也如此。循环 时, 是单调递增的。 - 每次新加入一个点
时,由于如上的单调性,点的横坐标是单调递增的。
因此我们在查询时,把队头斜率小于当前斜率的点弹出;插入时把队尾不在凸包上的点删去,其实就是让新读取的点代替原先的点。定量来说,就是:当 long long)。循环时只需要把
#include <bits/stdc++.h>#define N 300010using namespace std;
typedef long long ll;
ll dp[N];ll f[N], t[N];int q[N];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
int n, s; cin >> n >> s; for (int i = 1; i <= n; i++) { cin >> t[i] >> f[i]; f[i] += f[i - 1]; t[i] += t[i - 1]; } int hh = 0, tt = 0; q[0] = 0; for (int i = 1; i <= n; i++) { while (hh < tt && (dp[q[hh + 1]] - dp[q[hh]]) <= (t[i] + s) * (f[q[hh + 1]] - f[q[hh]])) hh++; int j = q[hh]; dp[i] = dp[j] - (t[i] + s) * f[j] + f[i] * t[i] + s * f[n]; while (hh < tt && (dp[q[tt]] - dp[q[tt - 1]]) * (f[i] - f[q[tt]]) >= (dp[i] - dp[q[tt]]) * (f[q[tt]] - f[q[tt - 1]])) tt--; q[++tt] = i; } cout << dp[n] << endl; return 0;}洛谷 P5785 [SDOI2012] 任务安排/AcWing 302 任务安排3
题目地址:P5785/AcWing 302
题目难度:省选/NOI-/困难
题目来源:山东 2012 各省省选
题面相同,数据范围中
需要注意的是,尽管待求直线的斜率 int j = q[hh] 替换成二分查找)。
由于本题数据较大,在交叉相乘时可能会爆,因此推荐使用 double 或 __int128。
#include <bits/stdc++.h>#define N 300010using namespace std;
typedef long long ll;
ll dp[N];ll f[N], t[N];int tt = 0;int q[N];
int upperbound(ll piv) { int L = 0, R = tt; while (L < R) { int mid = (L + R) >> 1; if ((__int128) (dp[q[mid + 1]] - dp[q[mid]]) > (__int128) (f[q[mid + 1]] - f[q[mid]]) * piv) R = mid; else L = mid + 1; } return q[L];}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
int n, s; cin >> n >> s; for (int i = 1; i <= n; i++) { cin >> t[i] >> f[i]; t[i] += t[i - 1]; f[i] += f[i - 1]; } q[0] = 0; for (int i = 1; i <= n; i++) { int j = upperbound(t[i] + s); dp[i] = dp[j] - (t[i] + s) * f[j] + f[i] * t[i] + s * f[n]; while (tt && (__int128) (dp[q[tt]] - dp[q[tt - 1]]) * (f[i] - f[q[tt]]) >= (__int128) (dp[i] - dp[q[tt]]) * (f[q[tt]] - f[q[tt - 1]])) tt--; q[++tt] = i; } cout << dp[n] << endl; return 0;}CF 311B Cats Transport
题目地址:CF 311B
题目难度:省选/NOI-
Zxr960115 是一个大农场主。
他养了
只可爱的猫子,雇佣了 个铲屎官。这里有一条又直又长的道路穿过了农场,有 个山丘坐落在道路周围,编号自左往右从 到 。山丘 与山丘 的距离是 米。铲屎官们住在 号山丘。 一天,猫子们外出玩耍。猫子
去山丘 游玩,在 时间结束他的游玩,然后在山丘 傻等铲屎官。铲屎官们必须把所有的猫子带上。每个铲屎官直接从 走到 ,中间不停下,可以认为不花费时间的把游玩结束的猫子带上。每个铲屎官的速度为一米每单位时间,并且足够强壮来带上任意数量的猫子。 举个栗子,假装我们有两个山丘(
),有一只猫子,他想去山丘 玩到时间 。然后铲屎官如果在时间 或者时间 从 号山丘出发,他就能抱走猫子。如果他在时间 出发那么就不行(猫子还在玩耍)。如果铲屎官在时间 出发,猫子就不用等他( )。如果他在时间 出发,猫子就要等他 个单位时间。 你的任务是安排每个铲屎官出发的时间(可以从 0 时刻之前出发),最小化猫子们等待的时间之和。
对于全部的数据,满足
, , , , , 。 输入格式:
输入的第一行是三个整数
。 第二行有
个整数,分别是 。 接下来
行,每行两个整数 。 输出格式:
一行一个整数,为最小化的等待时间之和。
在本题中,请务必使用 cin/cout 或者 %I64d 通配符来读入64位整数。

读题就能发现,我们的饲养员的行走时间和
接下来的转换非常巧妙:定义
假如我们的饲养员想要一次性接完排序
这道题还有一个硬性要求——饲养员的个数不超过 dp[i][j] 表示使用了 dp[k][j - 1],那么我们派出第 dp[i][j] = min(dp[i][j], dp[k][j - 1] + (i - k) * a[i] - (sumA[i] - sumA[k]))
同样的思路,我们把方程化简成
斜率是
#include <bits/stdc++.h>
#define N 200010#define P 110using namespace std;
typedef long long ll;
int q[N];ll dp[N][P], a[N];ll sumD[N], sumA[N];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
int n, m, p; cin >> n >> m >> p; for (int i = 2; i <= n; i++) { cin >> sumD[i]; sumD[i] += sumD[i - 1]; } for (int i = 1; i <= m; i++) { int h, t; cin >> h >> t; a[i] = t - sumD[h]; } sort(a + 1, a + 1 + m); for (int i = 1; i <= m; i++) sumA[i] = sumA[i - 1] + a[i];
memset(dp, 0x3f, sizeof dp); for (int i = 0; i <= p; i++) dp[0][i] = 0;
for (int j = 1; j <= p; j++) { int hh = 0, tt = 0; q[0] = 0; for (int i = 1; i <= m; i++) { while (hh < tt && dp[q[hh + 1]][j - 1] + sumA[q[hh + 1]] - dp[q[hh]][j - 1] - sumA[q[hh]] <= a[i] * (q[hh + 1] - q[hh])) hh++; dp[i][j] = dp[q[hh]][j - 1] + i * a[i] - q[hh] * a[i] - sumA[i] + sumA[q[hh]]; while (hh < tt && (dp[q[tt]][j - 1] + sumA[q[tt]] - dp[q[tt - 1]][j - 1] - sumA[q[tt - 1]]) * (i - q[tt]) >= (dp[i][j - 1] + sumA[i] - dp[q[tt]][j - 1] - sumA[q[tt]]) * (q[tt] - q[tt - 1])) tt--; q[++tt] = i; } } cout << dp[m][p] << endl; return 0;}支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!
部分内容可能已过时