任务安排1(小数据):https://www.luogu.com.cn/problem/P2365

任务安排2(大数据):https://www.luogu.com.cn/problem/P5785

题目描述

有 \(N\) 个任务排成一个序列在一台机器上等待执行,它们的顺序不得改变。机器会把这 \(N\) 个任务分成若干批,每一批包含连续的若干个任务。从时刻 \(0\) 开始,任务被分批加工,执行第 \(i\) 个任务所需的时间是 \(T_i\)。另外,在每批任务开始前,机器需要 \(S\) 的启动时间,故执行一批任务所需的时间是启动时间 \(S\) 加上每个任务所需时间之和。

一个任务执行后,将在机器中稍作等待,直至该批任务全部执行完毕。也就是说,同一批任务将在同一时刻完成。每个任务的费用是它的完成时刻乘以一个费用系数 \(C_i\)。

请为机器规划一个分组方案,使得费用最小。

输入格式

第一行是 \(N\) ,第二行是 \(S\)。

下面 \(N\) 行每行有一对数,分别为 \(T_i\) 和 \(C_i\),均为不大于 \(100\) 的正整数,表示第 \(i\) 个任务单独完成所需的时间是 \(T_i\) 机器费用系数 \(C_i\)。

输出格式

输出一个整数,表示最小的总费用。

样例输入

5
1
1 3
3 2
4 3
2 3
1 4

样例输出

153

数据规模

50%的数据保证 \(1 \lt N \le 5000, 1 \le S \le 50, 1 \le T_i, C_i \le 100\) ;

100%的数据保证 \(1 \le N \le 3 \times 10^5, 1 \le S,T_i,C_i \le 512\)。

问题分析

解法一:

求出 \(T,C\) 的前缀和 \(sumT,sumC\),即

\[sumT[i] = \sum_{j=1}^{i} T_j
\]

\[sumC[i] = \sum_{j=1}^{i} C_j
\]

设 \(F[i][j]\) 表示把前 \(i\) 个任务分成 \(j\) 批执行的最小费用,则第 \(j\) 批任务的完成时间就是 \(j \times S + sumT[i]\)。

以第 \(j-1\) 批和第 \(j\) 批任务的分界点为DP的“决策”,(设第 \(j-1\) 批的最后一个任务是 \(k\),第 \(j\) 批的最后一个任务是 \(i\))状态转移方程为:

\[F[i][j] = \min_{0 \le k \ \lt i} \{ F[k][j-1] + (S \times j + sumT[i]) \times (sumC[i] - sumC[k]) \}
\]

实现代码如下:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 5000;
int n, S, T, C, sumT[maxn], sumC[maxn], f[maxn][maxn], ans = -1;
int main() {
cin >> n >> S;
for (int i = 1; i <= n; i ++) {
cin >> T >> C;
sumT[i] = sumT[i-1] + T;
sumC[i] = sumC[i-1] + C;
}
memset(f, -1, sizeof(f));
for (int j = 1; j <= n; j ++) {
for (int i = j; i <= n; i ++) {
if (j == 1) {
f[i][j] = (S + sumT[i]) * sumC[i];
continue;
}
for (int k = j-1; k < i; k ++) {
assert(f[k][j-1] != -1);
int tmp = f[k][j-1] + (S * j + sumT[i]) * (sumC[i] - sumC[k]);
if (f[i][j] == -1 || f[i][j] > tmp) f[i][j] = tmp;
}
}
}
for (int i = 1; i <= n; i ++) {
if (ans == -1 || ans > f[n][i]) ans = f[n][i];
}
cout << ans << endl;
return 0;
}

该解法的时间复杂度是 \(O(n^3)\)。

解法二:

本题并没有规定需要把任务分成多少批,在上一个解法中之所以需要批数 \(j\),是因为我们需要知道机器启动了多少次(每次启动都要 \(S\) 单位时间),从而计算出 \(i\) 所在的一批任务的完成时刻。

事实上,在执行一批任务时,我们不容易直接得知在此之前机器启动过几次。但我们知道,机器因执行这批任务而花费的启动时间 \(S\),会累加到在此之后所有任务的完成时刻上。

设 \(F[i]\) 表示把前 \(i\) 个任务分成若干批执行的最小费用,状态转移方程为:

\[F[i] = \min \{ F[j] + sumT[i] \times (sumC[i] - sumC[j]) + S \times (sumC[N] - sumC[j]) \}
\]

在上式中,第 \(j+1 \sim i\) 个任务在同一批内完成,\(sumT[i]\) 是忽略机器的启动时间,这批任务的完成时刻。因为这批任务的执行,机器的启动时间 \(S\) 会对第 \(j+1\) 个之后的所有任务产生影响,故我们把这部分补充到费用中。

也就是说,我们没有直接求出每批任务的完成时间,而是在一批任务“开始”对后续任务产生影响时,就先把费用累加到结果中。这是一种名为 “费用提前计算” 的经典思想。

实现代码如下:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 5000;
int n, S, T, C, sumT[maxn], sumC[maxn], f[maxn];
int main() {
cin >> n >> S;
for (int i = 1; i <= n; i ++) {
cin >> T >> C;
sumT[i] = sumT[i-1] + T;
sumC[i] = sumC[i-1] + C;
}
memset(f, -1, sizeof(f));
f[0] = 0;
for (int i = 1; i <= n; i ++) {
for (int j = 0; j < i; j ++) {
int tmp = f[j] + sumT[i] * (sumC[i] - sumC[j]) + S * (sumC[n] - sumC[j]);
if (f[i] == -1 || f[i] > tmp) f[i] = tmp;
}
}
cout << f[n] << endl;
return 0;
}

该解法的时间复杂度为 \(O(N^2)\)。

解法三:

对上一题的算法二进行优化,先对状态转移方程稍作变形,把常数、仅与 \(i\) 有关的项、仅与 \(j\) 有关的项 以及 \(i,j\) 的乘积项分开。

\[F[i] = \min_{0 \le j \lt i} \{ F[j] - (S + sumT[i]) \times sumC[j] \} + sumT[i] \times sumC[i] + S \times sumC[N]
\]

把 \(\min\) 函数去掉,把关于 \(j\) 的值 \(F[j]\) 和 \(sumC[j]\) 看做变量,其余部分看做常数,得到:

\[F[j] = (S + sumT[i]) \times sumC[j] + F[i] - sumT[i] \times sumC[i] - S \times sumC[N]
\]

在 \(sumC[j]\) 为横坐标, \(F[j]\) 为纵坐标的平面直角坐标系中,这是一条以 \(S + sumT[i]\) 为斜率,\(F[i] - sumT[i] \times sumC[i] - S \times sumC[N]\) 为截距的直线。也就是说,决策候选集合是坐标系中的一个点集,每个决策 \(j\) 都对应着坐标系中的一个点 \((sumC[j], F[j])\)。每个待求解的状态 \(F[i]\) 都对应着一条直线的截距,直线的斜率是一个固定的值 \(S + sumT[i]\),截距未知。当截距最小化时,\(F[i]\) 也取到最小值。

该问题实际上是一个线性规划问题,高中数学有所涉及。令直线过每个决策点 \((sumC[j], F[j])\),都可以求得一个截距,其中使截距最小的一个就是最优决策。体现在坐标系中,就是用一条斜率为固定正整数的直线自下而上平移,第一次接触到某个决策点时,就得到了最小截距。如图所示:

对于任意三个决策点 \((sumC[j_1], F[j_1])\),\((sumC[j_2], F[j_2])\) 和 \((sumC[j_3], F[j_3])\),不妨设 \(j_1 \lt j_2 \lt j_3\),因为 \(T,C\) 均为正整数,亦有 \(sumC[j_1] \lt sumC[j_2] \lt sumC[j_3]\)。根据及时排除无用决策的思想,我们考虑 \(j_2\) 可能成为最优决策的条件。

从上图中我们发现,\(j_2\) 有可能成为最优决策,当且仅当 \(j_1\) 到 \(j_2\) 的斜率小于 \(j_2\) 到 \(j_3\) 的斜率,即:

\[\frac{F[j_2] - F[j_1]}{sumC[j_2] - sumC[j_1]} \lt \frac{F[j_3] - F[j_2]}{sumC[j_3] - sumC[j_2]}
\]

小于号两侧实际上都是连接两个决策点的线段的斜率。通俗地讲,我们应该维护“连接相邻两点的线段斜率”单调递增的一个“下凸壳”,只有这个“下凸壳”的顶点才有可能成为最优决策。实际上,对于一条斜率为 \(k\) 的直线,若某个顶点左侧线段线段的斜率比 \(k\) 小,右侧线段的斜率比 \(k\) 大,则该顶点就是最优决策。换言之,如果把这条直线和所有线段组成一个序列,那么令直线截距最小化的顶点就出现在按照斜率大小排序时,直线应该排在的位置上。如图所示:

在本题中,\(j\) 的取值范围是 \(0 \le j \lt i\),随着 \(i\) 的增大,每次会有一个新的决策进入候选集合。因为 \(sumC\) 的单调性,新决策在坐标系中的横坐标一定大于之前的所有决策,出现在凸壳的最右端。另外,因为 \(sumT\) 的单调性,每次求解“最小截距”的直线斜率 \(S+sumT[i]\) 也单调递增,如果我们只保留凸壳上“连接相邻两点的线段斜率”大于 \(S+sumT[i]\) 的部分,那么凸壳的最左端点就一定是最优决策。

综上所述,我们可以建立单调队列 \(q\),维护这个下凸壳。队列中保存若干个决策变量,它们对应凸壳上的顶点,且满足横坐标 \(sumC\) 递增、连接相邻两点的线段斜率也递增。需要支持的操作与一般的单调队列题目类似,对于每个状态变量 \(i\):

  1. 检查队首的两个决策变量 \(Q_l\) 和 \(Q_{l+1}\),若斜率 \(\frac{F[Q_{l+1}] - F[Q_l]}{sumC[Q_{l+1}] - sumC[Q_l]} \le S + sumT[i]\),则让 \(Q_l\) 出队,继续检查新的队首。
  2. 直接取队首 \(j = Q_l\) 为最优决策,执行状态转移,计算出 \(F[i]\)。
  3. 把新决策 \(i\) 从队尾插入,在插入之前,若三个决策点 \(j_1 = Q_{r-1}, j_2 = Q_r, j_3 = i\) 不满足斜率单调递增(不满足下凸性,即 \(j_2\) 是无用决策),则直接从队尾让 \(Q_r\) 出队,继续检查新的队尾。

实现代码如下:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 300030;
int n, q[maxn], l, r;
long long S, T, C, sumT[maxn], sumC[maxn], f[maxn];
int main() {
cin >> n >> S;
for (int i = 1; i <= n; i ++) {
cin >> T >> C;
sumT[i] = sumT[i-1] + T;
sumC[i] = sumC[i-1] + C;
}
memset(f, -1, sizeof(f));
f[0] = 0;
q[l = r = 1] = 0;
for (int i = 1; i <= n; i ++) {
while (l < r && f[q[l+1]] - f[q[l]] <= (S + sumT[i]) * (sumC[q[l+1]] - sumC[q[l]])) l ++;
f[i] = f[q[l]] - (S + sumT[i]) * sumC[q[l]] + sumT[i] * sumC[i] + S * sumC[n];
while (l < r && (f[q[r]]-f[q[r-1]]) * (sumC[i]-sumC[q[r]]) >= (f[i]-f[q[r]]) * (sumC[q[r]]-sumC[q[r-1]])) r --;
q[++r] = i;
}
cout << f[n] << endl;
return 0;
}

整个算法的时间复杂度为 \(O(N)\)。

与一般的单调队列优化DP的模型相比,本题维护的“单调性”依赖于队列中相邻两个元素之间的某种“比值”。因为这个值对应线性规划的坐标系中的斜率,所以我们在本题中使用的优化方法称为“斜率优化”。


以上分析针对 \(T_i\) 为正数的情况,接下来我们来考虑 \(T_i\) 为负数的情况。


与任务安排1不同的是,任务安排2中任务的执行时间 \(T\) 可能是负数。这意味着 \(sumT\) 不具有单调性,从而需要最小化截距的直线的斜率 \(S + sumT[i]\) 不具有单调性。所以,我们不能在单调队列中只保留凸壳上“连接相邻两点的线段斜率”大于 \(S + sumT[i]\) 的部分,而是必须维护整个凸壳。这样一来,我们就不需要在队首把斜率与 \(S + sumT[i]\) 比较。

队首也不一定是最优决策,我们可以在单调队列中二分查找,求出一个位置 \(p\),\(p\) 左侧线段的斜率比 \(S + sumT[i]\) 小,右侧线段的斜率比 \(S+sumT[i]\) 大,\(p\) 就是最优决策。

实现代码如下:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 300030;
int n, q[maxn], l, r;
long long S, T, C, sumT[maxn], sumC[maxn], f[maxn];
int my_binary_search(int k) {
if (l == r) return q[l];
int L = l, R = r;
while (L < R) {
int mid = (L + R) / 2;
if (f[q[mid+1]] - f[q[mid]] <= k * (sumC[q[mid+1]] - sumC[q[mid]])) L = mid + 1;
else R = mid;
}
return q[L];
}
int main() {
cin >> n >> S;
for (int i = 1; i <= n; i ++) {
cin >> T >> C;
sumT[i] = sumT[i-1] + T;
sumC[i] = sumC[i-1] + C;
}
memset(f, -1, sizeof(f));
f[0] = 0;
q[l = r = 1] = 0;
for (int i = 1; i <= n; i ++) {
int p = my_binary_search(S + sumT[i]);
f[i] = f[p] - (S + sumT[i]) * sumC[p] + sumT[i] * sumC[i] + S * sumC[n];
while (l < r && (f[q[r]]-f[q[r-1]]) * (sumC[i]-sumC[q[r]]) >= (f[i]-f[q[r]]) * (sumC[q[r]]-sumC[q[r-1]])) r --;
q[++r] = i;
}
cout << f[n] << endl;
return 0;
}

最新文章

  1. Sicily 1444: Prime Path(BFS)
  2. js模拟抛出球运动
  3. 详解用Navicat工具将Excel中的数据导入Mysql中
  4. NPOI导出EXCEL 打印设置分页及打印标题
  5. 为什么 SharedPreferences 可以直接 调用,前面却没有对象
  6. net start mysql服务名无效
  7. 自动帮助创建android资源xml文件的网站
  8. uc浏览器qq浏览器广告过滤
  9. PHP面试题(二)
  10. PHP安装OPENSSL扩展模块
  11. 前端javascript框架之BackboneJS学习笔记
  12. winDbg 命令使用帮助
  13. FastDFS单机版安装
  14. ionic3 小记录
  15. 【腾讯Bugly干货分享】那些年,我们一起写过的“单例模式”
  16. go语言程序设计学习笔记-1
  17. [转帖] infiniband的协议速度
  18. 微软BI SSRS 2012 Metro UI Win 8 风格的报表课程案例全展示
  19. 【Unix网络编程】chapter3套接字编程简介
  20. Entity Framework 6.1.0 Tools for Visual Studio 2012 &amp; 2013

热门文章

  1. 漏洞复现 MS11-003
  2. 网络框架OKHTTP使用场景全解析
  3. Jmeter(十二) - 从入门到精通 - JMeter逻辑控制器 - 终篇(详解教程)
  4. JavaWeb网上图书商城完整项目--day02-26.查询所有分类功能之DAO层实现
  5. APP测试之内存命令查询
  6. LQR要点
  7. 用JS实现改变文本框的只读属性
  8. 搭建nginx服务器nginx-1.6.2.tar.gz
  9. 十.总结drf视图
  10. linux terminal---EOF