题解:洛谷 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. 对同一个工件,每道工序必须在它前面的工序完成后才能开始;
  2. 同一时刻每一台机器至多只能加工一个工件。

另一方面,在安排后面的操作时,不能改动前面已安排的操作的工作状态。

由于同一工件都是按工序的顺序安排的,因此,只按原顺序给出工件号,仍可得到同样的安排顺序,于是,在输入数据中,我们将这个安排顺序简写为 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

【核心思想】

  1. 问题分析:有 \(n\) 个工件,每个工件需在 \(m\) 台不同机器上按固定顺序完成 \(m\) 道工序。给定一个操作安排顺序(由工件号组成,按序对应每个工件的工序),要求在满足“同一工件工序顺序”和“同一机器同一时刻只能加工一个工件”的约束下,按照“尽量靠前插入”的规则安排每个操作,求完成所有操作的最短总时间。本质是一个车间调度模拟问题,需要为每个操作在对应机器的可用时间段中找到最早的可插入位置。

  2. 算法选择

    • 逐操作安排:按照给定的操作顺序,依次安排每个操作。对于当前操作(工件 \(now\) 的第 \(step\) 道工序),已知其机器 \(m\) 和加工时间 \(t\),从该工件上一道工序完成时间之后开始,在机器 \(m\) 的时间轴上寻找一段长度至少为 \(t\) 的连续空闲区间,将操作安排在该区间的最早位置。
    • 时间轴模拟:使用二维数组 mac[machine][time] 标记机器在某个时刻是否被占用。寻找空闲区间时,遍历时间轴,统计连续空闲长度,找到足够长度后占用对应时间段。
    • 复杂度\(O(m \times n \times T)\),其中 \(T\) 是时间轴长度(答案),但 \(m,n \le 20\)\(T\) 也不会太大,完全可行。
  3. 关键步骤

    • 读入数据:读取 \(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] = jans = max(ans, j)
        • 跳出循环。
    • 输出ans
  4. 时间/空间复杂度

    • 时间复杂度:\(O(m \times n \times ans)\),由于 \(m, n \le 20\)\(ans\) 通常不大,完全可行。
    • 空间复杂度:\(O(m \times ans)\),时间轴长度设为足够大(如 100000)。
  5. 车间调度模拟的核心思想

    • 约束保证:从 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
posted @ 2026-08-31 09:23  团爸讲算法  阅读(6)  评论(0)    收藏  举报