任务调度 - 题解【动态规划】
题面
该题为CCF认证考试真题,试题编号为201403-5。原题链接见:201403-5:任务调度。现将题面搬运如下:
问题描述
有若干个任务需要在一台机器上运行。它们之间没有依赖关系,因此可以被按照任意顺序执行。
该机器有两个 CPU 和一个 GPU。对于每个任务,你可以为它分配不同的硬件资源:
1. 在单个 CPU 上运行。
2. 在两个 CPU 上同时运行。
3. 在单个 CPU 和 GPU 上同时运行。
4. 在两个 CPU 和 GPU 上同时运行。
一个任务开始执行以后,将会独占它所用到的所有硬件资源,不得中 断,直到执行结束为止。第 \(i\) 个任务用单个 CPU,两个 CPU,单个 CPU 加 GPU,两个 CPU 加 GPU 运行所消耗的时间分别为 \(a_i,b_i,c_i\) 和 \(d_i\)。
现在需要你计算出至少需要花多少时间可以把所有给定的任务完成。
输入格式
输入的第一行只有一个正整数 \(n \space (1 ≤ n ≤ 40)\), 是总共需要执行的任 务个数。
接下来的 \(n\) 行每行有四个正整数 \(a_i, b_i, c_i, d_i \space\) (\(a_i, b_i, c_i, d_i\) 均不超过 10), 以空格隔开。
输出格式
输出只有一个整数,即完成给定的所有任务所需的最少时间。
样例输入
3
4 4 2 2
7 4 7 4
3 3 3 3
样例输出
7
样例说明
有很多种调度方案可以在 7 个时间单位里完成给定的三个任务,以下是其中的一种方案:
同时运行第一个任务(单 CPU 加上 GPU)和第三个任务(单 CPU), 它们分别在时刻 2 和时刻 3 完成。在时刻 3 开始双 CPU 运行任务 2,在时刻 7 完成。
题解
考虑方案2和4,它们同时占用CPU1和CPU2。因此,无论在何时按照方案2还是方案4分配,其都不会与别的方案产生竞争:因为没有单独使用GPU资源的任务。而且,一旦使用方案2或者方案4,中间就不能插入别的任务,而方案1和方案3由于使用单个CPU,可能对资源产生争夺。因此,考虑将2和4方案合并,将可行方案减少到3种,将原方案1视作新方案0,原方案3视作新方案1,原方案2和4视作新方案2。下面不特殊说明时,均为新方案的编号。
假设第u个任务采用方案i所需要的机时为\(t[u][i]\)。考虑到有两个CPU,最坏情况下一半的任务在CPU1运行,一半任务在CPU2运行,则最长的机时为\(max(n)\times max(t) \div 2\),代入数据范围得\(40\times 10 \div 2 = 200\)。同样可以用这种方法计算当前数据的最大运行时长,记为\(wm\)。
考虑使用动态规划求解,\(dp[u][i][j][k]\)表示在前\(u\)个任务中,CPU1占用\(i\)时间,CPU2占用\(j\)时间,GPU占用\(k\)时间的前提下,采用方案2所耗费的总时长。这样可以得到状态转移方程:
可以写出核心代码如下:
dp[0][0][0][0] = 0;
rep(u, 1, n) {
rep(i, 0, wm) {
rep(j, 0, wm) {
rep(k, 0, wm) {
int &v = dp[u][i][j][k];
v = dp[u - 1][i][j][k] + t[u][2];
if (i >= t[u][0]) {
v = min(v, dp[u - 1][i - t[u][0]][j][k]);
}
if (j >= t[u][0]) {
v = min(v, dp[u - 1][i][j - t[u][0]][k]);
}
if (i >= t[u][1] && k >= t[u][1]) {
v = min(v, dp[u - 1][i - t[u][1]][j][k - t[u][1]]);
}
if (j >= t[u][1] && k >= t[u][1]) {
v = min(v, dp[u - 1][i][j - t[u][1]][k - t[u][1]]);
}
}
}
}
}
这样的话需要开辟一个 $40 \times 200 \times 200 \times 200 $ 大小的数组,而本题空间限制为256MB,实测开这么大的数组,空间可以飙到400+MB。因此,考虑对dp数组进行压缩。注意到,对于当前任务 \(u\) ,求解其耗费时间,只需要知道前一个任务 \(u-1\) 的信息,因此可以考虑使用滚动数组压缩,对于偶数使用维度0,奇数使用维度1,这样,在考虑第1个任务的时候只需知道前0个任务,就是在$ dp[1] $ 考虑 $ dp[0] $,而接下来求前2个任务时只需知道前1个任务,就是在 \(dp[0]\) 考虑 \(dp[1]\) ,而 \(dp[1]\) 在之前更新过了。至于当前使用的是维度0还是维度1,可以对2求余数进行选择。这样,就可以完成数组的压缩。优化过的核心代码如下:
dp[0][0][0][0] = 0;
rep(u, 1, n) {
rep(i, 0, wm) {
rep(j, 0, wm) {
rep(k, 0, wm) {
int &v = dp[u & 1][i][j][k];
int p=(u - 1) & 1;
v = dp[p][i][j][k] + t[u][2];
if (i >= t[u][0]) {
v = min(v, dp[p][i - t[u][0]][j][k]);
}
if (j >= t[u][0]) {
v = min(v, dp[p][i][j - t[u][0]][k]);
}
if (i >= t[u][1] && k >= t[u][1]) {
v = min(v, dp[p][i - t[u][1]][j][k - t[u][1]]);
}
if (j >= t[u][1] && k >= t[u][1]) {
v = min(v, dp[p][i][j - t[u][1]][k - t[u][1]]);
}
}
}
}
}
在最后,我们需要计算调度的最短时间。再提一下DP数组的含义:\(dp[u][i][j][k]\)代表前\(u\)个任务在CPU1机时\(i\),CPU2机时\(j\),GPU机时\(k\)的前提下,应用方案2耗费的总时长。就是说,DP数组内存储的是应用方案2的总时长,而应用方案0和方案1所耗费的机时隐藏在三个维度\(i,j,k\)中。在统计答案的时候,需要在计算方案2使用的总时长时,额外统计方案0、1使用的时长。这个时长当然是由CPU1机时、CPU2机时和GPU机时的最大值来决定。所以,我们可以得到最大机时统计核心代码:
ans = INT_MAX;
rep(i, 0, wm) {
rep(j, 0, wm) {
rep(k, 0, wm) {
ans = min(ans, dp[n & 1][i][j][k] + max(max(i, j), k));
}
}
}
代码
#include <bits/stdc++.h>
#define GRP int T;cin>>T;rep(C,1,T)
#define FAST ios::sync_with_stdio(false);cin.tie(0);
#define rep(i,a,b) for(int i=a;i<=b;++i)
#define rrep(i,a,b) for(int i=a;i>=b;--i)
#define elif else if
#define mem(arr,val) memset(arr,val,sizeof(arr))
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
int n;
int dp[2][202][202][202];
int t[42][3];
int a, b, c, d;
int wm, ans;
int main() {
FAST
cin >> n;
wm = 0;
rep(i, 1, n) {
cin >> a >> b >> c >> d;
t[i][0] = a;
t[i][1] = c;
t[i][2] = min(b, d);
wm += max(t[i][0], max(t[i][1], t[i][2]));
}
wm = (wm + 1) / 2;
mem(dp, 0x3f3f3f3f);
dp[0][0][0][0] = 0;
rep(u, 1, n) {
rep(i, 0, wm) {
rep(j, 0, wm) {
rep(k, 0, wm) {
int &v = dp[u & 1][i][j][k];
int p = (u - 1) & 1;
v = dp[p][i][j][k] + t[u][2];
if (i >= t[u][0]) {
v = min(v, dp[p][i - t[u][0]][j][k]);
}
if (j >= t[u][0]) {
v = min(v, dp[p][i][j - t[u][0]][k]);
}
if (i >= t[u][1] && k >= t[u][1]) {
v = min(v, dp[p][i - t[u][1]][j][k - t[u][1]]);
}
if (j >= t[u][1] && k >= t[u][1]) {
v = min(v, dp[p][i][j - t[u][1]][k - t[u][1]]);
}
}
}
}
}
ans = INT_MAX;
rep(i, 0, wm) {
rep(j, 0, wm) {
rep(k, 0, wm) {
ans = min(ans, dp[n & 1][i][j][k] + max(max(i, j), k));
}
}
}
cout << ans << endl;
return 0;
}
/*
_ _ _ _
/\ | | | | | | (_)
/ \ | | _____ _| |__| | ___ _ __ _ _ __ __ _
/ /\ \ | |/ _ \ \/ / __ |/ _ \| '__| | '_ \ / _` |
/ ____ \| | __/> <| | | | (_) | | | | | | | (_| |
/_/ \_\_|\___/_/\_\_| |_|\___/|_| |_|_| |_|\__, |
__/ |
|___/
*/