题解:洛谷 P1065 作业调度方案
【题目来源】
洛谷:P1065 [NOIP 2006 提高组] 作业调度方案 - 洛谷(luogu.com.cn)
【题目描述】
我们现在要利用 \(m\) 台机器加工 \(n\) 个工件,每个工件都有 \(m\) 道工序,每道工序都在不同的指定的机器上完成。每个工件的每道工序都有指定的加工时间。
每个工件的每个工序称为一个操作,我们用记号 j-k 表示一个操作,其中 \(j\) 为 \(1\) 到 \(n\) 中的某个数字,为工件号;\(k\) 为 \(1\) 到 \(m\) 中的某个数字,为工序号,例如 2-4 表示第 \(2\) 个工件第 \(4\) 道工序的这个操作。在本题中,我们还给定对于各操作的一个安排顺序。
例如,当 \(n=3,m=2\) 时,1-1,1-2,2-1,3-1,3-2,2-2 就是一个给定的安排顺序,即先安排第 \(1\) 个工件的第 \(1\) 个工序,再安排第 \(1\) 个工件的第 \(2\) 个工序,然后再安排第 \(2\) 个工件的第 \(1\) 个工序,等等。
一方面,每个操作的安排都要满足以下的两个约束条件。
- 对同一个工件,每道工序必须在它前面的工序完成后才能开始;
- 同一时刻每一台机器至多只能加工一个工件。
另一方面,在安排后面的操作时,不能改动前面已安排的操作的工作状态。
由于同一工件都是按工序的顺序安排的,因此,只按原顺序给出工件号,仍可得到同样的安排顺序,于是,在输入数据中,我们将这个安排顺序简写为 1 1 2 3 3 2。
还要注意,“安排顺序”只要求按照给定的顺序安排每个操作。不一定是各机器上的实际操作顺序。在具体实施时,有可能排在后面的某个操作比前面的某个操作先完成。
例如,取 \(n=3,m=2\),已知数据如下(机器号/加工时间):

则对于安排顺序 1 1 2 3 3 2,下图中的两个实施方案都是正确的。但所需要的总时间分别是 \(10\) 与 \(12\)。

当一个操作插入到某台机器的某个空档时(机器上最后的尚未安排操作的部分也可以看作一个空档),可以靠前插入,也可以靠后或居中插入。为了使问题简单一些,我们约定:在保证约束条件(1)(2)的条件下,尽量靠前插入。并且,我们还约定,如果有多个空档可以插入,就在保证约束条件(1)(2)的条件下,插入到最前面的一个空档。于是,在这些约定下,上例中的方案一是正确的,而方案二是不正确的。
显然,在这些约定下,对于给定的安排顺序,符合该安排顺序的实施方案是唯一的,请你计算出该方案完成全部任务所需的总时间。
【输入】
第 \(1\) 行为两个正整数 \(m,n\),用一个空格隔开,(其中 \(m(\lt 20)\) 表示机器数,\(n(\lt 20)\) 表示工件数)
第 2 行:\(m\times n\) 个用空格隔开的数,为给定的安排顺序。
接下来的 \(2n\) 行,每行都是用空格隔开的 \(m\) 个正整数,每个数不超过 \(20\)。
其中前 \(n\) 行依次表示每个工件的每个工序所使用的机器号,第 \(1\) 个数为第 \(1\) 个工序的机器号,第 \(2\) 个数为第 \(2\) 个工序机器号,等等。
后 \(n\) 行依次表示每个工件的每个工序的加工时间。
可以保证,以上各数据都是正确的,不必检验。
【输出】
\(1\) 个正整数,为最少的加工时间。
【输入样例】
2 3
1 1 2 3 3 2
1 2
1 2
2 1
3 2
2 5
2 4
【输出样例】
10
【核心思想】
-
问题分析:有 \(n\) 个工件,每个工件需在 \(m\) 台不同机器上按固定顺序完成 \(m\) 道工序。给定一个操作安排顺序(由工件号组成,按序对应每个工件的工序),要求在满足“同一工件工序顺序”和“同一机器同一时刻只能加工一个工件”的约束下,按照“尽量靠前插入”的规则安排每个操作,求完成所有操作的最短总时间。本质是一个车间调度模拟问题,需要为每个操作在对应机器的可用时间段中找到最早的可插入位置。
-
算法选择:
- 逐操作安排:按照给定的操作顺序,依次安排每个操作。对于当前操作(工件 \(now\) 的第 \(step\) 道工序),已知其机器 \(m\) 和加工时间 \(t\),从该工件上一道工序完成时间之后开始,在机器 \(m\) 的时间轴上寻找一段长度至少为 \(t\) 的连续空闲区间,将操作安排在该区间的最早位置。
- 时间轴模拟:使用二维数组
mac[machine][time]标记机器在某个时刻是否被占用。寻找空闲区间时,遍历时间轴,统计连续空闲长度,找到足够长度后占用对应时间段。 - 复杂度:\(O(m \times n \times T)\),其中 \(T\) 是时间轴长度(答案),但 \(m,n \le 20\),\(T\) 也不会太大,完全可行。
-
关键步骤:
- 读入数据:读取 \(m, n\),操作顺序
list[1..m*n],每个工件每道工序的机器号machine[工件][工序]和加工时间time[工件][工序]。 - 初始化:
step[now]记录每个工件当前已完成的工序数;last[now]记录每个工件上一道工序的完成时间;mac[机器][时间] = 0表示空闲。 - 按顺序安排:遍历
list中的每个工件号now:step[now]++,得到当前工序号k。- 获取机器
ma = machine[now][k],加工时长t = time[now][k]。 - 从时间
last[now] + 1开始,在机器ma的时间轴上找连续空闲段:- 用
cnt记录当前连续空闲长度。 - 遇到空闲则
cnt++,否则cnt = 0。 - 当
cnt == t时,找到位置start = j - t + 1。 - 将
mac[ma][start..j]标记为占用。 - 更新
last[now] = j,ans = max(ans, j)。 - 跳出循环。
- 用
- 输出:
ans。
- 读入数据:读取 \(m, n\),操作顺序
-
时间/空间复杂度:
- 时间复杂度:\(O(m \times n \times ans)\),由于 \(m, n \le 20\),\(ans\) 通常不大,完全可行。
- 空间复杂度:\(O(m \times ans)\),时间轴长度设为足够大(如 100000)。
-
车间调度模拟的核心思想:
- 约束保证:从
last[now] + 1开始搜索,确保同工件工序顺序约束;在机器时间轴上找空闲段,确保同机器同一时刻不冲突。 - 贪心插入:在满足约束的前提下,尽量靠前插入,即找到最早的可用空闲段。
- 时间轴标记:用数组标记机器占用情况,模拟离散时间点(因为加工时间都是整数)。
- 适用场景:这类“多机调度”问题是模拟题中的复杂类型,考查数组操作和贪心插入策略。
- 约束保证:从
【算法标签】
普及 #模拟
【代码详解】
#include <iostream>
using namespace std;
// 定义结构体,存储每个工件在每个工序的信息
struct information
{
int id; // 机器编号
int cost; // 加工时间
} info[22][22]; // info[i][j] 表示第i个工件的第j个工序的信息
int list[404] = {0}; // 工件的加工顺序列表
int step[22] = {0}; // 记录每个工件当前进行到第几个工序
int last_time[22] = {0}; // 记录每个工件上一次加工完成的时间
int mac[22][100000] = {0}; // 记录每台机器的占用情况,mac[i][j]=1表示第i台机器在第j时刻被占用
int ans = 0; // 记录所有工件完成加工的最晚时间
int main()
{
int m, n; // m 表示机器数量,n 表示工件数量
cin >> m >> n; // 输入机器数量和工件数量
// 输入工件的加工顺序
for (int i = 1; i <= m * n; i++)
cin >> list[i];
// 输入每个工件的每个工序所使用的机器编号
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> info[i][j].id;
// 输入每个工件的每个工序的加工时间
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> info[i][j].cost;
// 模拟加工过程
for (int i = 1; i <= m * n; i++)
{
int now = list[i]; // 当前要加工的工件编号
step[now]++; // 当前工件的工序数加1
int machine = info[now][step[now]].id; // 当前工序使用的机器编号
int time = info[now][step[now]].cost; // 当前工序的加工时间
int s = 0; // 记录连续空闲时间段的长度
// 从上次加工完成时间+1开始,寻找一个连续的空闲时间段
for (int j = last_time[now] + 1; ; j++)
{
if (mac[machine][j] == 0) // 如果当前时刻机器空闲
s++; // 连续空闲时间加1
else
s = 0; // 如果机器被占用,重置连续空闲时间
// 如果找到足够长的连续空闲时间段
if (s == time)
{
// 占用这段时间
for (int k = j - time + 1; k <= j; k++)
mac[machine][k] = 1; // 标记机器在这段时间被占用
// 更新所有工件完成加工的最晚时间
if (j > ans)
ans = j;
// 更新当前工件的上一次加工完成时间
last_time[now] = j;
break; // 跳出循环
}
}
}
// 输出所有工件完成加工的最晚时间
cout << ans;
return 0;
}
【运行结果】
2 3
1 1 2 3 3 2
1 2
1 2
2 1
3 2
2 5
2 4
10
浙公网安备 33010602011771号