前一篇文章《使用.NET Core與Google Optimization Tools實現員工排班計划Scheduling》算是一種針對內容的規划,而針對時間順序任務規划,加工車間的工活兒是一個典型的場景。在加工車間有不同的工活兒,一般稱為作業,每種作業都有多道工序,每道工序只能在特定的機器上完成。工序有不同的時長,而且是不能更改先后的。這些作業正是制造車間大規模生產線的任務,比如汽車零件制造。問題就是,工廠需要做一個最優的規划,使得作業嚴格按工序進行的前提下,消耗的時間最短,這樣就保證了生產效率是最佳的。如果想做到最優規划,以下約束必不可少:
1. 在作業中必須要前一道工序完成才能進行下一道工序。
2. 對於一台機器,一次只能支持一個作業中的一道工序的運轉。
3. 對於每道工序,一旦開始就必須完整地結束。
案例背景
以下是某個車間的作業情況,job代表作業,(m,p)代表了工序,其中m表示從0開始的機器編號,p代表了這道工序需要消耗的時長。本文假設了一個作業計划:
job 0 = [(0, 3), (1, 2), (2, 2)]
job 1 = [(0, 2), (2, 1), (1, 4)]
job 2 = [(1, 4), (2, 3)]
如上所示,job 0有三道工序,第一道工序在0號機器用掉3個單位時長,第二道在1號機器用掉2個單位時長,第三道在2號機器用掉2個單位時長,以此類推,總共八道工序。
解決方案
有一種解決方案如下圖,在一個時間軸上,每道工序有一個開始時間,占據一定的時長代表消耗部分,互相不會重疊,所有工序安置完畢,最長的地方就是整個作業全部完成的時長。

上圖給了一個示范,一共消耗12個單位時長,當然這也不是最優的,后面通過編碼我們再計算出最優的結果。
定義約束
首先我們將工序時長定義為task(i, j),表示job i的第j道工序,定義ti, j表示task(i, j)開始的時間點。有了這兩種定義,按照之間的要求,於是有了如下的關系約束:
1. 連接約束,對於同一個作業,前一道工序加上消耗的時長就是后一道工序。比如,對於作業job 0來說,t0, 2表示第二道工序開始的位置,最多消耗2個單位時長之后,就是第三道工序的位置,即:t0, 2 + 2 ≤ t0, 3。
2. 非連接約束,對於不同的作業,要保證前一道工序完成后才能進行下一道工序。比如在1號的機器上有task(0, 2)和task(2, 1),它們消耗的時長分別是2和4個單位,那么就有:
t0, 2 + 2 ≤ t2, 1 如果task(0, 2)在task(2, 1)前運行的話
或者
t2, 1 + 4 ≤ t0, 2 如果task(2, 1)在task(0, 2)前運行的話
基於這個關系,前面案例的作業計划的約束關系如圖所示:

帶箭頭的實線表示了每個作業的工序,有連接約束的情況,而虛線表示了非連接約束的情況,實線有箭頭是因為每個作業的工序是確定的,而虛線沒有箭頭也就說明順序是沒有確定的,這也正是我們要通過規划解決的問題。
最終求解目標
如果假定pi, j表示task(i, j)的消耗時長,那么我們要解決的全局問題就是在所有task都完成后,求一個maxi, j ti, j + pi, j的最小值,表示生產效率最優的結果。
代碼分解
看過本文開頭談到的前一篇文章后,對於項目初始化和相同的基本概念就不再介紹了。
首先定義一些初始化用的值。
// 創建約束求解器. var solver = new Solver("jobshop"); var machines_count = 3; var jobs_count = 3; var all_machines = Enumerable.Range(0, machines_count); var all_jobs = Enumerable.Range(0, jobs_count);
再定義出所有的工序。MakeFixedDurationIntervalVar就是OR-Tools專門用來創建間隔時間的變量類型。
// 將任務拆分成對應的機器和用時的結構 // job 0 = [(0, 3), (1, 2), (2, 2)] // job 1 = [(0, 2), (2, 1), (1, 4)] // job 2 = [(1, 4), (2, 3)] var machines = new int[][] { new[] { 0, 1, 2 }, new[] { 0, 2, 1 }, new[] { 1, 2 } }; var processing_times = new int[][] { new[] { 3, 2, 2 }, new[] {2, 1, 4 }, new[] { 4, 3 } }; // 計算總用時 var horizon = 0; foreach (var i in all_jobs) horizon += processing_times[i].Sum(); // 創建工序變量 var all_tasks = new Dictionary<(int, int), IntervalVar>(); foreach (var i in all_jobs) { foreach (var j in Enumerable.Range(0, machines[i].Length)) { all_tasks[(i, j)] = solver.MakeFixedDurationIntervalVar(0, horizon, processing_times[i][j], false, $"Job_{i}_{j}"); } }
然后定義連接約束和非連接約束,MakeDisjunctiveConstraints專門用來創建非連接約束的,StartsAfterEnd專門用來創建連接約束。
// 創建連接的順序變量及連接關系 var all_sequences = new SequenceVarVector(); //var all_machines_jobs = new List<IntervalVar>(); foreach (var i in all_machines) { var machines_jobs = new IntervalVarVector(); foreach (var j in all_jobs) { foreach (var k in Enumerable.Range(0, machines[j].Length)) { if (machines[j][k] == i) { machines_jobs.Add(all_tasks[(j, k)]); } } } var disj = solver.MakeDisjunctiveConstraint(machines_jobs, $"machine {i}"); all_sequences.Add(disj.SequenceVar()); solver.Add(disj); } // 定義連接約束 foreach (var i in all_jobs) { foreach (var j in Enumerable.Range(0, machines[i].Length - 1)) { solver.Add(all_tasks[(i, j + 1)].StartsAfterEnd(all_tasks[(i, j)])); } }
重點的部分,就是創建求解目標了。MakeMinimize用來求最小值,第二個參數表示每次移動的步長,直到有解為止。
// 創建求解的極值目標 var end_tasks = new IntVarVector(); foreach (var i in all_jobs) { end_tasks.Add(all_tasks[(i, machines[i].Length - 1)].EndExpr().Var()); } var obj_var = solver.MakeMax(end_tasks); var objective_monitor = solver.MakeMinimize(obj_var.Var(), 1); // 創建求解的對象 var sequence_phase = solver.MakePhase(all_sequences.ToArray(), Solver.SEQUENCE_DEFAULT); var vars_phase = solver.MakePhase(new[] { obj_var.Var() }, Solver.CHOOSE_FIRST_UNBOUND, Solver.ASSIGN_MIN_VALUE); var main_phase = solver.Compose(new[] { sequence_phase, vars_phase });
最后是顯示最優解結果的部分。
// 創建最后一個解決方案 var collector = solver.MakeLastSolutionCollector(); // 添加需要關注的變量 collector.Add(all_sequences.ToArray()); collector.AddObjective(obj_var.Var()); foreach (var i in all_machines) { var sequence = all_sequences[i]; var sequence_count = sequence.Size(); for (var j = 0; j < sequence_count; j++) { var t = sequence.Interval(j); collector.Add(t.StartExpr().Var()); collector.Add(t.EndExpr().Var()); } } // 顯示結果 var disp_col_width = 10; if (solver.Solve(main_phase, new SearchMonitor[] { objective_monitor, collector })) { Console.WriteLine("\nOptimal Schedule Length: {0}\n", collector.ObjectiveValue(0)); var sol_line = ""; var sol_line_tasks = ""; Console.WriteLine("Optimal Schedule\n"); foreach (var i in all_machines) { var seq = all_sequences[i]; sol_line += $"Machine {i}: "; sol_line_tasks += $"Machine {i}: "; var sequence = collector.ForwardSequence(0, seq); var seq_size = sequence.Count; foreach (var j in Enumerable.Range(0, seq_size)) { var t = seq.Interval(sequence[j]); sol_line_tasks += t.Name().PadRight(disp_col_width, ' '); } foreach (var j in Enumerable.Range(0, seq_size)) { var t = seq.Interval(sequence[j]); var sol_tmp = $"[{collector.Value(0, t.StartExpr().Var())},{collector.Value(0, t.EndExpr().Var())}]"; sol_line += sol_tmp.PadRight(disp_col_width, ' '); } sol_line += "\n"; sol_line_tasks += "\n"; } Console.WriteLine(sol_line_tasks); Console.WriteLine("Time Intervals for Tasks\n"); Console.WriteLine(sol_line); }
運行后結果如下:

可以看到,這一次求得了最優解,與前面給的示范的結果不一樣了,總時長上更少,是11而不是12了。對應的圖解是這樣:

是不是覺得很有趣,躍躍欲試了!動手做就是最好的開始。
最后放出完整代碼:
using Google.OrTools.ConstraintSolver; using System; using System.Collections.Generic; using System.Linq; public class ConsoleApp1 { static void Main() { // 創建約束求解器. var solver = new Solver("jobshop"); var machines_count = 3; var jobs_count = 3; var all_machines = Enumerable.Range(0, machines_count); var all_jobs = Enumerable.Range(0, jobs_count); // 將任務拆分成對應的機器和用時的結構 // job 0 = [(0, 3), (1, 2), (2, 2)] // job 1 = [(0, 2), (2, 1), (1, 4)] // job 2 = [(1, 4), (2, 3)] var machines = new int[][] { new[] { 0, 1, 2 }, new[] { 0, 2, 1 }, new[] { 1, 2 } }; var processing_times = new int[][] { new[] { 3, 2, 2 }, new[] {2, 1, 4 }, new[] { 4, 3 } }; // 計算總用時 var horizon = 0; foreach (var i in all_jobs) horizon += processing_times[i].Sum(); // 創建工序變量 var all_tasks = new Dictionary<(int, int), IntervalVar>(); foreach (var i in all_jobs) { foreach (var j in Enumerable.Range(0, machines[i].Length)) { all_tasks[(i, j)] = solver.MakeFixedDurationIntervalVar(0, horizon, processing_times[i][j], false, $"Job_{i}_{j}"); } } // 創建連接的順序變量及連接關系 var all_sequences = new SequenceVarVector(); //var all_machines_jobs = new List<IntervalVar>(); foreach (var i in all_machines) { var machines_jobs = new IntervalVarVector(); foreach (var j in all_jobs) { foreach (var k in Enumerable.Range(0, machines[j].Length)) { if (machines[j][k] == i) { machines_jobs.Add(all_tasks[(j, k)]); } } } var disj = solver.MakeDisjunctiveConstraint(machines_jobs, $"machine {i}"); all_sequences.Add(disj.SequenceVar()); solver.Add(disj); } // 定義連接約束 foreach (var i in all_jobs) { foreach (var j in Enumerable.Range(0, machines[i].Length - 1)) { solver.Add(all_tasks[(i, j + 1)].StartsAfterEnd(all_tasks[(i, j)])); } } // 創建求解的極值目標 var end_tasks = new IntVarVector(); foreach (var i in all_jobs) { end_tasks.Add(all_tasks[(i, machines[i].Length - 1)].EndExpr().Var()); } var obj_var = solver.MakeMax(end_tasks); var objective_monitor = solver.MakeMinimize(obj_var.Var(), 1); // 創建求解的對象 var sequence_phase = solver.MakePhase(all_sequences.ToArray(), Solver.SEQUENCE_DEFAULT); var vars_phase = solver.MakePhase(new[] { obj_var.Var() }, Solver.CHOOSE_FIRST_UNBOUND, Solver.ASSIGN_MIN_VALUE); var main_phase = solver.Compose(new[] { sequence_phase, vars_phase }); // 創建最后一個解決方案 var collector = solver.MakeLastSolutionCollector(); // 添加需要關注的變量 collector.Add(all_sequences.ToArray()); collector.AddObjective(obj_var.Var()); foreach (var i in all_machines) { var sequence = all_sequences[i]; var sequence_count = sequence.Size(); for (var j = 0; j < sequence_count; j++) { var t = sequence.Interval(j); collector.Add(t.StartExpr().Var()); collector.Add(t.EndExpr().Var()); } } // 顯示結果 var disp_col_width = 10; if (solver.Solve(main_phase, new SearchMonitor[] { objective_monitor, collector })) { Console.WriteLine("\nOptimal Schedule Length: {0}\n", collector.ObjectiveValue(0)); var sol_line = ""; var sol_line_tasks = ""; Console.WriteLine("Optimal Schedule\n"); foreach (var i in all_machines) { var seq = all_sequences[i]; sol_line += $"Machine {i}: "; sol_line_tasks += $"Machine {i}: "; var sequence = collector.ForwardSequence(0, seq); var seq_size = sequence.Count; foreach (var j in Enumerable.Range(0, seq_size)) { var t = seq.Interval(sequence[j]); sol_line_tasks += t.Name().PadRight(disp_col_width, ' '); } foreach (var j in Enumerable.Range(0, seq_size)) { var t = seq.Interval(sequence[j]); var sol_tmp = $"[{collector.Value(0, t.StartExpr().Var())},{collector.Value(0, t.EndExpr().Var())}]"; sol_line += sol_tmp.PadRight(disp_col_width, ' '); } sol_line += "\n"; sol_line_tasks += "\n"; } Console.WriteLine(sol_line_tasks); Console.WriteLine("Time Intervals for Tasks\n"); Console.WriteLine(sol_line); } } }
