# [APCS] 工作排程 ###### tags: `APCS` ## 題目 ### 問題描述 有 $M$ 個工作要在 $N$ 台機器上加工,每個工作 $i$ 包含若干個工序 $o_{ij}$,這些工序必須依序加工,也就是前一道工序 $o_{i(j-1)}$ 完成後才可以開始下一道工序 $o_{ij}$。每一道工序 $o_{ij}$ 可以用一個有序對 $(k_{ij}, t_{ij})$ 來 表示它需要在機器 $k_{ij}$ 上面花費 $t_{ij}$ 小時來完成。**每台機器一次只能處理一道工序**。 所謂一道工序 $o_{ij}$ 的**最早完成時間** $c_{ij}^{*}$ 是指,考慮目前排程中機器 $k_{ij}$ 之可用性以及前一道工序 $o_{i(j-1)}$(如果該工序存在)之完成時間後可得的最早完成時間。 $$ c_{ij}^{*} = \max(機器k_{ij}最早可用時間, c_{i(j-1)}^{*}) + t_{ij} $$ 工廠經理安排所有工序的排程規則如下: > 針對每一個工作的第一個尚未排程的工序,計算出此工序的**最早完成時間**,然後挑選出最早完成時間最小的工序納入排程,如果有多個最早完成時間都是最小,則挑選其中工作編號最小之工序。一個工序一旦納入排程就不會再更改。重複以上步驟直到所有工序皆納入排程。 我們總是從時間 0 開始排程。每個工作的完成時間為其最後一個工序的完成時間,本題的目標是計算出每個工作的完成時間並輸出其總和。 以下以一個例子來說明,在此例中有三個工作要在三台機器上排程,各工作的資料如下: ![](https://hackmd.io/_uploads/BJuanQyV3.png) 排程的過程說明如下: 1. 在開始時,每個工作都是考慮第一道工序,三個工作的第 1 道工序需要的時間分別是 $t_{11} = 4$、$t_{21} = 2$、$t_{31} = 7$,這也是它們的最早完成時間,也就是 $c_{11}^*=4$、$c_{21}^*=2$、 $c_{31}^*=7$,因此會先排 $o_{21}$。 ![](https://hackmd.io/_uploads/By-c6QyEh.png) 2. 接下來,三個工作要考慮的工序分別是第1、2、1個工序,即 $o_{11}$、$o_{22}$ 和 $o_{31}$。 * $o_{11}$ 需要機器 2 執行 4 小時 ,而機器 2 可以開始加工的時間點是 0。$o_{11}$沒有前一道工序。因此,這工序可以開始的時間是 $\max(0, 0) = 0$。因此,其最早完成時間 $c_{11}^*= \max(0, 0) + 4 = 4$。 * $o_{22}$ 需要機 器 2 執行 2 小時,而機器 2 可以開始加工的時間點是 0。$o_{22}$ 前一道工序 $o_{21}$ 的完成時間是 2。因此,這工序可以開始的時間是 $\max(0, 2) = 2$。因此,其最早完成時間 $c_{22}^* = \max(0, 2) + 2 = 4$。 * $o_{31}$ 需要機器 0 執行 7 小時,而機器 0 可以開始加工的時間點是 2。$o_{31}$ 沒有前一道工序。因此,這工序可以開始的時間是 $\max(2, 0) = 2$。因此,其最早完成時間 $c_{31}^* = \max(2, 0) + 7 = 9$。因此,由於 $c_{11}^*$ 和 $c_{22}^*$ 都是最小,根據規則工作編號小的先排,所以會排 $o_{11}$。 ![](https://hackmd.io/_uploads/HkZLy4kNh.png) 3. 三個工作目前要考慮的工序分別第 2、2、1個工序。依照類似的推論,我們可以得到 $c_{12}^* = 5$、$c_{22}^* = 6$、$c_{31}^* = 9$,因此排 $o_{12}$。工作 1 的工序均已排完,所以它的完成時間是 5。 4. 剩下工作 2 與 3。$c_{22}^* = 6$、$c_{31}^* = 9$,因此先排 $o_{22}$。 5. $c_{23}^* = 7$ 而 $c_{31}^* = 9$,因此排 $o_{23}$,工作 2 的工序已排完,所以它的完成時間是 7。 6. 剩下工作 3,因為機器 0 的下一個可以開始時間是 7,故 $o_{31}$ 的完成時間是 $7+7=14$。 ![](https://hackmd.io/_uploads/S1YRzV1N2.png) 三個工作的完成時間分別是5、7、14,所以最後輸出答案 $5+7+14=26$。 ### 輸入格式 第一行有兩個整數 $N$ 與 $M$,代表總共有 $N$ 台機器與 $M$ 個工作。接下來有 $M$ 個工作的資訊,輸入的順序即是工作編號順序。每個工作資訊包含兩行,第一行是整數 $P$,代表該工作的工序的數量;第二行是 $2 \cdot P$ 個整數,每兩個一組依序代表一道工序的機器編號與需求時間。 機器的編號由 0 開始。參數 $N$、$M$、$P$ 以及每個工序的需求時間都是不超過 100 的正整數。 ### 輸出格式 輸出所有工作的完成時間的總和。 ![](https://hackmd.io/_uploads/BkOYW41V3.png =50%x) ## 解答 ### 題目分析 這個題目看起來題目敘述很長,題目又涉及似乎很高深的工作排程,或許讓一些人望之卻步。其實題目敘述本身不長,是例子的說明比較長,而APCS的考試不會要求基本數學以外的背景知識,所以這個題目完全不需要擔心不懂排程的背景知識。這種題目是屬於模擬題,題目中會敘述一個流程,希望你模擬這個流程來計算某些數值。模擬題的重點是**設定適當的變數記錄某些狀態,根據流程來逐步更新這些狀態**。 在本題中,有 $M$ 個工作與 $N$ 台機器,每個工作有數個工序 (task) 必須依序進行,每個 task 需要某台特定機器 以及需要若干時間,最重要的流程就是安排 task 的規則: 針對每一個工作的第一個尚未排程的工序,計算出此工序的最早完成時間,然後挑選出最早完成時間最小的工序納入排程,如果有多個最早完成時間都是最小,則挑選其中工作編號最小之工序。 對於每一個工作,它的下一個 task 的最早完成時間是什麼呢?應該是**這個 task 可以開始的時間,加上它所需要的時間**。那麼,它可以開始的時間又是什麼呢?依據定義它必須在它的前一個 task 完成後才能開始,而且它所需要的機器必須是空下來的。所以,在模擬的過程,除了輸入的資料之外,我們必須去記錄: * 對每一個工作 $i$,下一個要安排的 task:`curr_task[i]` * 對每一個工作 $i$,前一個 task 的完成時間:`prev_fin[i]` * 對於每一個機器 $j$,下一個可以開始的時間:`avl[j]` ### 方法與流程 程式的架構其實很簡單,用一個迴圈每次安排一個 task,直到所有 task 都安排完畢。迴圈中,計算每一個工作下一個 task 的可能最早完成時間,並且找出最小值。找到之後,將此 task 排入機器,並更新機器與工作的狀態。程式架構如下: ![](https://hackmd.io/_uploads/Hkoe5VJEn.png =60%x) 迴圈內每次尋找 task 最早完成時間的最小值的時候,我們都重新計算,事實上,利用優先佇列(priority queue) 或類似資料結構,可以更有效率地完成此事。 ### 程式實作 ```python= ##### 輸入資料處理部分 ##### # 吃第一行輸入資料 # 包含機器(machine)數量n_machine # 和工作(job)數量n_job n_machine, n_job = map(int, input().split()) # 為每個job初始化一個裝數對的list # 這些list再組成一個list of list job = [[] for _ in range(n_job)] # 初始化工序(task)的總數 total_task = 0 # 吃剩下的輸入資料 # 對於屬於每一個job的一組資料去做迭代 for i in range(n_job): # 第一行是該job的task總數 k = int(input()) # 把該job的task數加到task總數上 total_task += k # 用來裝該job所帶有的task資訊 tasks = [int(x) for x in input().split()] # 再把這些task的資訊拆分裝到job這個list裡面 for j in range(0, 2 * k, 2): job[i].append((tasks[j], tasks[j + 1])) # 儲存每個machine目前的可使用時間 avl = [0] * n_machine # 儲存每個job目前執行到的task的完成時間 prev_fin = [0] * n_job # 儲存目前每個job執行到的task編號 curr_task = [0] * n_job ##### 演算法部分 ##### # 對於每個task去做迭代 # 順序未知,但我們知道總共有total_task個task # 把完成時間最早的task放進去排程裡 # 若有多個同時完成的task,選擇其job編號最小的task for _ in range(total_task): # 用一個可能的最大值初始化最早(最小)的時間 earliest_time = float("inf") earliest_job = earliest_machine = -1 # 對每個job去做迭代 for i in range(n_job): if curr_task[i] >= len(job[i]): # 如果目前這個job執行到的task編號大於等於該job的task總數 # 就跳過這次迴圈執行 continue else: # 計算對於第i個job的最早完成時間 # 先拆分目前這個task所需要的machine和該task所需的時間長度 curr_machine, curr_task_len = job[i][curr_task[i]] # 按照題目給的思路找出完成時間 curr_time = max(avl[curr_machine], prev_fin[i]) + curr_task_len if curr_time < earliest_time: earliest_time = curr_time earliest_job = i earliest_machine = curr_machine # 更新決定排上去的task的資訊上去 avl[earliest_machine] = earliest_time curr_task[earliest_job] += 1 prev_fin[earliest_job] = earliest_time # 印出要求的解答 print(sum(prev_fin)) ```