任务安排1(小数据):https://www.luogu.com.cn/problem/P2365
任务安排2(大数据):https://www.luogu.com.cn/problem/P5785c++
有 \(N\) 个任务排成一个序列在一台机器上等待执行,它们的顺序不得改变。机器会把这 \(N\) 个任务分红若干批,每一批包含连续的若干个任务。从时刻 \(0\) 开始,任务被分批加工,执行第 \(i\) 个任务所需的时间是 \(T_i\)。另外,在每批任务开始前,机器须要 \(S\) 的启动时间,故执行一批任务所需的时间是启动时间 \(S\) 加上每一个任务所需时间之和。算法
一个任务执行后,将在机器中稍做等待,直至该批任务所有执行完毕。也就是说,同一批任务将在同一时刻完成。每一个任务的费用是它的完成时刻乘以一个费用系数 \(C_i\)。函数
请为机器规划一个分组方案,使得费用最小。大数据
第一行是 \(N\) ,第二行是 \(S\)。优化
下面 \(N\) 行每行有一对数,分别为 \(T_i\) 和 \(C_i\),均为不大于 \(100\) 的正整数,表示第 \(i\) 个任务单独完成所需的时间是 \(T_i\) 机器费用系数 \(C_i\)。spa
输出一个整数,表示最小的总费用。code
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\)。blog
求出 \(T,C\) 的前缀和 \(sumT,sumC\),即排序
设 \(F[i][j]\) 表示把前 \(i\) 个任务分红 \(j\) 批执行的最小费用,则第 \(j\) 批任务的完成时间就是 \(j \times S + sumT[i]\)。队列
以第 \(j-1\) 批和第 \(j\) 批任务的分界点为DP的“决策”,(设第 \(j-1\) 批的最后一个任务是 \(k\),第 \(j\) 批的最后一个任务是 \(i\))状态转移方程为:
实现代码以下:
#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\) 个任务分红若干批执行的最小费用,状态转移方程为:
在上式中,第 \(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\) 的乘积项分开。
把 \(\min\) 函数去掉,把关于 \(j\) 的值 \(F[j]\) 和 \(sumC[j]\) 看作变量,其他部分看作常数,获得:
在 \(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\) 的斜率,即:
小于号两侧实际上都是链接两个决策点的线段的斜率。通俗地讲,咱们应该维护“链接相邻两点的线段斜率”单调递增的一个“下凸壳”,只有这个“下凸壳”的顶点才有可能成为最优决策。实际上,对于一条斜率为 \(k\) 的直线,若某个顶点左侧线段线段的斜率比 \(k\) 小,右侧线段的斜率比 \(k\) 大,则该顶点就是最优决策。换言之,若是把这条直线和全部线段组成一个序列,那么令直线截距最小化的顶点就出如今按照斜率大小排序时,直线应该排在的位置上。如图所示:
在本题中,\(j\) 的取值范围是 \(0 \le j \lt i\),随着 \(i\) 的增大,每次会有一个新的决策进入候选集合。由于 \(sumC\) 的单调性,新决策在坐标系中的横坐标必定大于以前的全部决策,出如今凸壳的最右端。另外,由于 \(sumT\) 的单调性,每次求解“最小截距”的直线斜率 \(S+sumT[i]\) 也单调递增,若是咱们只保留凸壳上“链接相邻两点的线段斜率”大于 \(S+sumT[i]\) 的部分,那么凸壳的最左端点就必定是最优决策。
综上所述,咱们能够创建单调队列 \(q\),维护这个下凸壳。队列中保存若干个决策变量,它们对应凸壳上的顶点,且知足横坐标 \(sumC\) 递增、链接相邻两点的线段斜率也递增。须要支持的操做与通常的单调队列题目相似,对于每一个状态变量 \(i\):
实现代码以下:
#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; }