任务调度 - 题解【动态规划】

题面

该题为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[u][i][j][k]=\left\{ \begin{array}{lr} 0,u=i=j=k=0 \\ \verb|min|( \\ \verb| |dp[u-1][i][j][k]+t[u][2], \verb| \\采用调度2| \\ \verb| |dp[u-1][i-t[u][0]][j][k], \verb| \\采用调度0,分配给CPU1| \\ \verb| |dp[u-1][i][j-t[u][0]][k],\verb| \\采用调度0,分配给CPU2|\\ \verb| |dp[u-1][i-t[u][1]][j][k-t[u][1]],\verb| \\采用调度1,分配给CPU1与GPU|\\ \verb| |dp[u-1][i][j-t[u][1]][k-t[u][1]]\verb| \\采用调度1,分配给CPU2与GPU|\\ ),\verb|others| \end{array} \right. \]

可以写出核心代码如下:

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;
}

/*
          _           _    _            _
    /\   | |         | |  | |          (_)
   /  \  | | _____  _| |__| | ___  _ __ _ _ __   __ _
  / /\ \ | |/ _ \ \/ /  __  |/ _ \| '__| | '_ \ / _` |
 / ____ \| |  __/>  <| |  | | (_) | |  | | | | | (_| |
/_/    \_\_|\___/_/\_\_|  |_|\___/|_|  |_|_| |_|\__, |
                                                 __/ |
                                                |___/
*/
posted @ 2022-07-24 01:17  AlexHoring  阅读(475)  评论(0编辑  收藏  举报