OR-Tools 作业车间调度:模型讲解与四种语言完整示例
本文以中文讲解 OR-Tools CP-SAT 的作业车间调度模型,并附完整 Python、C++、Java 和 C# 实现。作业车间调度(job shop scheduling)把一组有先后次序的工序安排到多台机器上。每道工序需要指定机器和加工时长;同一台机器一次只能处理一道工序,而且工序开始后不能暂停。教程为每道工序建立开始时间、结束时间和区间变量,再加入机器互斥及作业内的先后约束,最后最小化所有作业的完工时间跨度。
译写自 Google OR-Tools 指南:The Job Shop Problem。来源页未单列个人作者,本文归属 Google OR-Tools / Google Developers;页面正文标注 CC BY 4.0,代码示例标注 Apache License 2.0。本文翻译并整理说明,注明与原文的差异;图为原创绘制。下方四份完整程序按来源页的 Apache 2.0 条款转载,代码块仅移除网页显示缩进,没有改动程序内容;许可证全文随稿附在 LICENSE-APACHE-2.0.txt。来源页未单列个人作者或提供独立 NOTICE 文件。

先把工序序列写成数据
示例用二元组 (机器编号, 加工时长) 表示每道工序,机器和作业编号都从 0 开始:
作业 0 = [(0, 3), (1, 2), (2, 2)]
作业 1 = [(0, 2), (2, 1), (1, 4)]
作业 2 = [(1, 4), (2, 3)]
因此总计有 3 台机器、8 道工序。作业 0 必须先在机器 0 加工 3 个时间单位,再在机器 1 加工 2 个单位,最后在机器 2 加工 2 个单位。作业 1 和作业 2 也按各自数组顺序执行。时间单位可以是分钟、秒或抽象刻度,但所有时长与约束要使用同一单位。
来源页面的一处自然语言解释把 task(0, 2) 称作作业 0 的“第二道”工序,并将其对应到 (1, 2)。这与从零开始的代码索引不一致:task(0, 1) 才是作业 0 的第二道工序 (1, 2);task(0, 2) 是第三道工序 (2, 2)。本文按数组和代码使用的零起始索引修正了这个序号说明。
每道工序对应一个固定长度区间
对作业 i 的第 j 道工序,创建整数变量 start[i,j] 和 end[i,j],再建立长度为该工序处理时间的 interval。区间变量表达“从开始到结束持续处理”;模型要求 end - start = duration。起止时间都限制在允许的时域内,来源示例把各工序时长总和用作上界:
机器数 = 所有 machine_id 的最大值 + 1
horizon = 所有工序 duration 的总和
对每个工序创建 start、end 和固定时长 interval
这个 horizon 是安全上界:即便完全不并行、把所有工序串行做完,总时长也不超过各工序时长之和;最优方案不会比这种可行串行排程更长。真实问题如有释放时间、截止时间或跨班次约束,应另行建模,不能直接照搬这个上界或基础示例。
两类约束让安排可执行
机器互斥:把分配到同一台机器的全部 interval 放进同一组,并加入 add_no_overlap() 约束。求解器可以选择每道工序的开始时间,但不允许同一机器上的任意两道工序时间重叠。
作业内先后关系:对同一个作业相邻的工序加入 start[下一道] ≥ end[上一道]。这既阻止后续工序提前开始,也允许两道工序之间存在空闲等待。来源示例没有规定等待或机器切换的成本;若现场有清洗、换模或搬运时间,需要把它们作为额外持续时间或序列相关约束加入。
这两类约束缺一不可。只有机器互斥时,作业内工序可能乱序;只有工序先后时,多道工序可能同时占用同一台机器。区间默认不可抢占,不能在加工中间暂停再续做。
最小化最后一道工序的最晚结束时间
每个作业的完成时间是该作业最后一道工序的结束时间。建立变量 makespan,令它等于这些结束时间的最大值,然后最小化这个变量。也就是说,目标是尽早完成所有作业,而不是让每台机器负载相同、让总加工时间最少,或优先完成某一个作业。
在来源示例中,页面先展示了一个长度为 12 的可行排程,随后求解器报告长度为 11 的解。页面给出的一种最优结果如下;区间用左闭右开语义表示,所以同一台机器上的前一道工序结束时,下一道可立即开始:
| 机器 | 作业与工序顺序 | 时间区间 |
|---|---|---|
| 0 | 作业 0 工序 0 → 作业 1 工序 0 | [0, 3) → [3, 5) |
| 1 | 作业 2 工序 0 → 作业 0 工序 1 → 作业 1 工序 2 | [0, 4) → [4, 6) → [7, 11) |
| 2 | 作业 1 工序 1 → 作业 0 工序 2 → 作业 2 工序 1 | [5, 6) → [6, 8) → [8, 11) |
作业 1 的最后一道工序在机器 1 上从时间 7 开始;把它提前到时间 6 也可满足约束,但不会让整体完工时间低于 11,因为其他作业仍在时间 11 才结束。因此最优排程可能不唯一。应检查每个作业的先后关系和同机区间是否冲突,而不是要求复现唯一的时间表。
正确报告求解器状态
CP-SAT 返回的 OPTIMAL 表示找到了满足约束的解并证明目标最优;FEASIBLE 只表示找到了可行解,可能尚未证明它最优。来源代码把两种状态都放进输出分支,却统一打印 “Optimal Schedule Length”。这是标签可能误导的状态处理问题。应分别报告状态:只有 OPTIMAL 才标记为已证明最优;FEASIBLE 应说当前可行方案,并附上界、求解时间或最优差距(若有)。其他状态不能把变量值当作有效排程直接打印。
实现边界与版本
页面给出 Python、C++、Java 和 C# 版本;建模顺序相同,但类名、方法命名和 API 形式不同。当前 Python 示例使用小写的 new_int_var、new_interval_var、add_no_overlap 和 solve 等方法,不要把较旧版本的 API 名称与当前代码混用。来源页最后更新于 2024 年 8 月 28 日,未固定完整发行版本;复制代码前要按所安装的 OR-Tools 版本核对 API 与 CP-SAT 文档。
模型来自网页中的小型内存数据,没有文件读写、shell 调用、凭据或外部输入处理;本稿不含可执行的完整程序。本次仅做静态审阅,没有安装 OR-Tools,也没有执行任何代码或示例、运行模型或基准测试;来源页展示的 11 个时间单位结果未经本稿独立验证。图表中的数值来自来源页展示的方案,不是本稿测试结果。
完整示例程序
以下 Python、C++、Java 和 C# 程序逐段转载自来源指南的完整示例,使用上文的三组作业和八道工序。官方页面说明正文为 CC BY 4.0、代码样例为 Apache License 2.0;此处保留代码原貌,去掉的只有网页代码块外层缩进。完整 Apache 2.0 许可文本见随稿的 LICENSE-APACHE-2.0.txt。本文没有执行这些程序,运行前应按实际 OR-Tools 版本核对 API。
阅读状态分支时请留意,上下文中已说明源码把 OPTIMAL 和 FEASIBLE 都打印为 “Optimal Schedule Length”;只有 OPTIMAL 表示已证明最优。C++ 源码首行保留了来源页面中提及 nurse scheduling 的注释,它与本例用途不一致,但不改变程序逻辑。
Python
"""Minimal jobshop example."""
import collections
from ortools.sat.python import cp_model
def main() -> None:
"""Minimal jobshop problem."""
# Data.
jobs_data = [ # task = (machine_id, processing_time).
[(0, 3), (1, 2), (2, 2)], # Job0
[(0, 2), (2, 1), (1, 4)], # Job1
[(1, 4), (2, 3)], # Job2
]
machines_count = 1 + max(task[0] for job in jobs_data for task in job)
all_machines = range(machines_count)
# Computes horizon dynamically as the sum of all durations.
horizon = sum(task[1] for job in jobs_data for task in job)
# Create the model.
model = cp_model.CpModel()
# Named tuple to store information about created variables.
task_type = collections.namedtuple("task_type", "start end interval")
# Named tuple to manipulate solution information.
assigned_task_type = collections.namedtuple(
"assigned_task_type", "start job index duration"
)
# Creates job intervals and add to the corresponding machine lists.
all_tasks = {}
machine_to_intervals = collections.defaultdict(list)
for job_id, job in enumerate(jobs_data):
for task_id, task in enumerate(job):
machine, duration = task
suffix = f"_{job_id}_{task_id}"
start_var = model.new_int_var(0, horizon, "start" + suffix)
end_var = model.new_int_var(0, horizon, "end" + suffix)
interval_var = model.new_interval_var(
start_var, duration, end_var, "interval" + suffix
)
all_tasks[job_id, task_id] = task_type(
start=start_var, end=end_var, interval=interval_var
)
machine_to_intervals[machine].append(interval_var)
# Create and add disjunctive constraints.
for machine in all_machines:
model.add_no_overlap(machine_to_intervals[machine])
# Precedences inside a job.
for job_id, job in enumerate(jobs_data):
for task_id in range(len(job) - 1):
model.add(
all_tasks[job_id, task_id + 1].start >= all_tasks[job_id, task_id].end
)
# Makespan objective.
obj_var = model.new_int_var(0, horizon, "makespan")
model.add_max_equality(
obj_var,
[all_tasks[job_id, len(job) - 1].end for job_id, job in enumerate(jobs_data)],
)
model.minimize(obj_var)
# Creates the solver and solve.
solver = cp_model.CpSolver()
status = solver.solve(model)
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
print("Solution:")
# Create one list of assigned tasks per machine.
assigned_jobs = collections.defaultdict(list)
for job_id, job in enumerate(jobs_data):
for task_id, task in enumerate(job):
machine = task[0]
assigned_jobs[machine].append(
assigned_task_type(
start=solver.value(all_tasks[job_id, task_id].start),
job=job_id,
index=task_id,
duration=task[1],
)
)
# Create per machine output lines.
output = ""
for machine in all_machines:
# Sort by starting time.
assigned_jobs[machine].sort()
sol_line_tasks = "Machine " + str(machine) + ": "
sol_line = " "
for assigned_task in assigned_jobs[machine]:
name = f"job_{assigned_task.job}_task_{assigned_task.index}"
# add spaces to output to align columns.
sol_line_tasks += f"{name:15}"
start = assigned_task.start
duration = assigned_task.duration
sol_tmp = f"[{start},{start + duration}]"
# add spaces to output to align columns.
sol_line += f"{sol_tmp:15}"
sol_line += "\n"
sol_line_tasks += "\n"
output += sol_line_tasks
output += sol_line
# Finally print the solution found.
print(f"Optimal Schedule Length: {solver.objective_value}")
print(output)
else:
print("No solution found.")
# Statistics.
print("\n Statistics")
print(f" - conflicts: {solver.num_conflicts}")
print(f" - branches : {solver.num_branches}")
print(f" - wall time: {solver.wall_time}s")
if __name__ == "__main__":
main()
C++
// Nurse scheduling problem with shift requests.
#include <stdlib.h>
#include <algorithm>
#include <cstdint>
#include <map>
#include <numeric>
#include <string>
#include <tuple>
#include <vector>
#include "absl/base/log_severity.h"
#include "absl/log/globals.h"
#include "absl/strings/str_format.h"
#include "ortools/base/init_google.h"
#include "ortools/base/logging.h"
#include "ortools/sat/cp_model.h"
#include "ortools/sat/cp_model.pb.h"
#include "ortools/sat/cp_model_solver.h"
namespace operations_research {
namespace sat {
void MinimalJobshopSat() {
using Task = std::tuple<int64_t, int64_t>; // (machine_id, processing_time)
using Job = std::vector<Task>;
std::vector<Job> jobs_data = {
{{0, 3}, {1, 2}, {2, 2}}, // Job_0: Task_0 Task_1 Task_2
{{0, 2}, {2, 1}, {1, 4}}, // Job_1: Task_0 Task_1 Task_2
{{1, 4}, {2, 3}}, // Job_2: Task_0 Task_1
};
int64_t num_machines = 0;
for (const auto& job : jobs_data) {
for (const auto& [machine, _] : job) {
num_machines = std::max(num_machines, 1 + machine);
}
}
std::vector<int> all_machines(num_machines);
std::iota(all_machines.begin(), all_machines.end(), 0);
// Computes horizon dynamically as the sum of all durations.
int64_t horizon = 0;
for (const auto& job : jobs_data) {
for (const auto& [_, time] : job) {
horizon += time;
}
}
// Creates the model.
CpModelBuilder cp_model;
struct TaskType {
IntVar start;
IntVar end;
IntervalVar interval;
};
using TaskID = std::tuple<int, int>; // (job_id, task_id)
std::map<TaskID, TaskType> all_tasks;
std::map<int64_t, std::vector<IntervalVar>> machine_to_intervals;
for (int job_id = 0; job_id < jobs_data.size(); ++job_id) {
const auto& job = jobs_data[job_id];
for (int task_id = 0; task_id < job.size(); ++task_id) {
const auto [machine, duration] = job[task_id];
std::string suffix = absl::StrFormat("_%d_%d", job_id, task_id);
IntVar start = cp_model.NewIntVar({0, horizon})
.WithName(std::string("start") + suffix);
IntVar end = cp_model.NewIntVar({0, horizon})
.WithName(std::string("end") + suffix);
IntervalVar interval = cp_model.NewIntervalVar(start, duration, end)
.WithName(std::string("interval") + suffix);
TaskID key = std::make_tuple(job_id, task_id);
all_tasks.emplace(key, TaskType{/*.start=*/start,
/*.end=*/end,
/*.interval=*/interval});
machine_to_intervals[machine].push_back(interval);
}
}
// Create and add disjunctive constraints.
for (const auto machine : all_machines) {
cp_model.AddNoOverlap(machine_to_intervals[machine]);
}
// Precedences inside a job.
for (int job_id = 0; job_id < jobs_data.size(); ++job_id) {
const auto& job = jobs_data[job_id];
for (int task_id = 0; task_id < job.size() - 1; ++task_id) {
TaskID key = std::make_tuple(job_id, task_id);
TaskID next_key = std::make_tuple(job_id, task_id + 1);
cp_model.AddGreaterOrEqual(all_tasks[next_key].start, all_tasks[key].end);
}
}
// Makespan objective.
IntVar obj_var = cp_model.NewIntVar({0, horizon}).WithName("makespan");
std::vector<IntVar> ends;
for (int job_id = 0; job_id < jobs_data.size(); ++job_id) {
const auto& job = jobs_data[job_id];
TaskID key = std::make_tuple(job_id, job.size() - 1);
ends.push_back(all_tasks[key].end);
}
cp_model.AddMaxEquality(obj_var, ends);
cp_model.Minimize(obj_var);
const CpSolverResponse response = Solve(cp_model.Build());
if (response.status() == CpSolverStatus::OPTIMAL ||
response.status() == CpSolverStatus::FEASIBLE) {
LOG(INFO) << "Solution:";
// create one list of assigned tasks per machine.
struct AssignedTaskType {
int job_id;
int task_id;
int64_t start;
int64_t duration;
bool operator<(const AssignedTaskType& rhs) const {
return std::tie(this->start, this->duration) <
std::tie(rhs.start, rhs.duration);
}
};
std::map<int64_t, std::vector<AssignedTaskType>> assigned_jobs;
for (int job_id = 0; job_id < jobs_data.size(); ++job_id) {
const auto& job = jobs_data[job_id];
for (int task_id = 0; task_id < job.size(); ++task_id) {
const auto [machine, duration] = job[task_id];
TaskID key = std::make_tuple(job_id, task_id);
int64_t start = SolutionIntegerValue(response, all_tasks[key].start);
assigned_jobs[machine].push_back(
AssignedTaskType{/*.job_id=*/job_id,
/*.task_id=*/task_id,
/*.start=*/start,
/*.duration=*/duration});
}
}
// Create per machine output lines.
std::string output = "";
for (const auto machine : all_machines) {
// Sort by starting time.
std::sort(assigned_jobs[machine].begin(), assigned_jobs[machine].end());
std::string sol_line_tasks = "Machine " + std::to_string(machine) + ": ";
std::string sol_line = " ";
for (const auto& assigned_task : assigned_jobs[machine]) {
std::string name = absl::StrFormat(
"job_%d_task_%d", assigned_task.job_id, assigned_task.task_id);
// Add spaces to output to align columns.
sol_line_tasks += absl::StrFormat("%-15s", name);
int64_t start = assigned_task.start;
int64_t duration = assigned_task.duration;
std::string sol_tmp =
absl::StrFormat("[%i,%i]", start, start + duration);
// Add spaces to output to align columns.
sol_line += absl::StrFormat("%-15s", sol_tmp);
}
output += sol_line_tasks + "\n";
output += sol_line + "\n";
}
// Finally print the solution found.
LOG(INFO) << "Optimal Schedule Length: " << response.objective_value();
LOG(INFO) << "\n" << output;
} else {
LOG(INFO) << "No solution found.";
}
// Statistics.
LOG(INFO) << "Statistics";
LOG(INFO) << CpSolverResponseStats(response);
}
} // namespace sat
} // namespace operations_research
int main(int argc, char* argv[]) {
InitGoogle(argv[0], &argc, &argv, true);
absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo);
operations_research::sat::MinimalJobshopSat();
return EXIT_SUCCESS;
}
Java
package com.google.ortools.sat.samples;
import static java.lang.Math.max;
import com.google.ortools.Loader;
import com.google.ortools.sat.CpModel;
import com.google.ortools.sat.CpSolver;
import com.google.ortools.sat.CpSolverStatus;
import com.google.ortools.sat.IntVar;
import com.google.ortools.sat.IntervalVar;
import com.google.ortools.sat.LinearExpr;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
/** Minimal Jobshop problem. */
public class MinimalJobshopSat {
public static void main(String[] args) {
Loader.loadNativeLibraries();
class Task {
int machine;
int duration;
Task(int machine, int duration) {
this.machine = machine;
this.duration = duration;
}
}
final List<List<Task>> allJobs =
Arrays.asList(Arrays.asList(new Task(0, 3), new Task(1, 2), new Task(2, 2)), // Job0
Arrays.asList(new Task(0, 2), new Task(2, 1), new Task(1, 4)), // Job1
Arrays.asList(new Task(1, 4), new Task(2, 3)) // Job2
);
int numMachines = 1;
for (List<Task> job : allJobs) {
for (Task task : job) {
numMachines = max(numMachines, 1 + task.machine);
}
}
final int[] allMachines = IntStream.range(0, numMachines).toArray();
// Computes horizon dynamically as the sum of all durations.
int horizon = 0;
for (List<Task> job : allJobs) {
for (Task task : job) {
horizon += task.duration;
}
}
// Creates the model.
CpModel model = new CpModel();
class TaskType {
IntVar start;
IntVar end;
IntervalVar interval;
}
Map<List<Integer>, TaskType> allTasks = new HashMap<>();
Map<Integer, List<IntervalVar>> machineToIntervals = new HashMap<>();
for (int jobID = 0; jobID < allJobs.size(); ++jobID) {
List<Task> job = allJobs.get(jobID);
for (int taskID = 0; taskID < job.size(); ++taskID) {
Task task = job.get(taskID);
String suffix = "_" + jobID + "_" + taskID;
TaskType taskType = new TaskType();
taskType.start = model.newIntVar(0, horizon, "start" + suffix);
taskType.end = model.newIntVar(0, horizon, "end" + suffix);
taskType.interval = model.newIntervalVar(
taskType.start, LinearExpr.constant(task.duration), taskType.end, "interval" + suffix);
List<Integer> key = Arrays.asList(jobID, taskID);
allTasks.put(key, taskType);
machineToIntervals.computeIfAbsent(task.machine, (Integer k) -> new ArrayList<>());
machineToIntervals.get(task.machine).add(taskType.interval);
}
}
// Create and add disjunctive constraints.
for (int machine : allMachines) {
List<IntervalVar> list = machineToIntervals.get(machine);
model.addNoOverlap(list);
}
// Precedences inside a job.
for (int jobID = 0; jobID < allJobs.size(); ++jobID) {
List<Task> job = allJobs.get(jobID);
for (int taskID = 0; taskID < job.size() - 1; ++taskID) {
List<Integer> prevKey = Arrays.asList(jobID, taskID);
List<Integer> nextKey = Arrays.asList(jobID, taskID + 1);
model.addGreaterOrEqual(allTasks.get(nextKey).start, allTasks.get(prevKey).end);
}
}
// Makespan objective.
IntVar objVar = model.newIntVar(0, horizon, "makespan");
List<IntVar> ends = new ArrayList<>();
for (int jobID = 0; jobID < allJobs.size(); ++jobID) {
List<Task> job = allJobs.get(jobID);
List<Integer> key = Arrays.asList(jobID, job.size() - 1);
ends.add(allTasks.get(key).end);
}
model.addMaxEquality(objVar, ends);
model.minimize(objVar);
// Creates a solver and solves the model.
CpSolver solver = new CpSolver();
CpSolverStatus status = solver.solve(model);
if (status == CpSolverStatus.OPTIMAL || status == CpSolverStatus.FEASIBLE) {
class AssignedTask {
int jobID;
int taskID;
int start;
int duration;
// Ctor
AssignedTask(int jobID, int taskID, int start, int duration) {
this.jobID = jobID;
this.taskID = taskID;
this.start = start;
this.duration = duration;
}
}
class SortTasks implements Comparator<AssignedTask> {
@Override
public int compare(AssignedTask a, AssignedTask b) {
if (a.start != b.start) {
return a.start - b.start;
} else {
return a.duration - b.duration;
}
}
}
System.out.println("Solution:");
// Create one list of assigned tasks per machine.
Map<Integer, List<AssignedTask>> assignedJobs = new HashMap<>();
for (int jobID = 0; jobID < allJobs.size(); ++jobID) {
List<Task> job = allJobs.get(jobID);
for (int taskID = 0; taskID < job.size(); ++taskID) {
Task task = job.get(taskID);
List<Integer> key = Arrays.asList(jobID, taskID);
AssignedTask assignedTask = new AssignedTask(
jobID, taskID, (int) solver.value(allTasks.get(key).start), task.duration);
assignedJobs.computeIfAbsent(task.machine, (Integer k) -> new ArrayList<>());
assignedJobs.get(task.machine).add(assignedTask);
}
}
// Create per machine output lines.
String output = "";
for (int machine : allMachines) {
// Sort by starting time.
Collections.sort(assignedJobs.get(machine), new SortTasks());
String solLineTasks = "Machine " + machine + ": ";
String solLine = " ";
for (AssignedTask assignedTask : assignedJobs.get(machine)) {
String name = "job_" + assignedTask.jobID + "_task_" + assignedTask.taskID;
// Add spaces to output to align columns.
solLineTasks += String.format("%-15s", name);
String solTmp =
"[" + assignedTask.start + "," + (assignedTask.start + assignedTask.duration) + "]";
// Add spaces to output to align columns.
solLine += String.format("%-15s", solTmp);
}
output += solLineTasks + "%n";
output += solLine + "%n";
}
System.out.printf("Optimal Schedule Length: %f%n", solver.objectiveValue());
System.out.printf(output);
} else {
System.out.println("No solution found.");
}
// Statistics.
System.out.println("Statistics");
System.out.printf(" conflicts: %d%n", solver.numConflicts());
System.out.printf(" branches : %d%n", solver.numBranches());
System.out.printf(" wall time: %f s%n", solver.wallTime());
}
private MinimalJobshopSat() {}
}
C#
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Google.OrTools.Sat;
public class ScheduleRequestsSat
{
private class AssignedTask : IComparable
{
public int jobID;
public int taskID;
public int start;
public int duration;
public AssignedTask(int jobID, int taskID, int start, int duration)
{
this.jobID = jobID;
this.taskID = taskID;
this.start = start;
this.duration = duration;
}
public int CompareTo(object obj)
{
if (obj == null)
return 1;
AssignedTask otherTask = obj as AssignedTask;
if (otherTask != null)
{
if (this.start != otherTask.start)
return this.start.CompareTo(otherTask.start);
else
return this.duration.CompareTo(otherTask.duration);
}
else
throw new ArgumentException("Object is not a Temperature");
}
}
public static void Main(String[] args)
{
var allJobs =
new[] {
new[] {
// job0
new { machine = 0, duration = 3 }, // task0
new { machine = 1, duration = 2 }, // task1
new { machine = 2, duration = 2 }, // task2
}
.ToList(),
new[] {
// job1
new { machine = 0, duration = 2 }, // task0
new { machine = 2, duration = 1 }, // task1
new { machine = 1, duration = 4 }, // task2
}
.ToList(),
new[] {
// job2
new { machine = 1, duration = 4 }, // task0
new { machine = 2, duration = 3 }, // task1
}
.ToList(),
}
.ToList();
int numMachines = 0;
foreach (var job in allJobs)
{
foreach (var task in job)
{
numMachines = Math.Max(numMachines, 1 + task.machine);
}
}
int[] allMachines = Enumerable.Range(0, numMachines).ToArray();
// Computes horizon dynamically as the sum of all durations.
int horizon = 0;
foreach (var job in allJobs)
{
foreach (var task in job)
{
horizon += task.duration;
}
}
// Creates the model.
CpModel model = new CpModel();
Dictionary<Tuple<int, int>, Tuple<IntVar, IntVar, IntervalVar>> allTasks =
new Dictionary<Tuple<int, int>, Tuple<IntVar, IntVar, IntervalVar>>(); // (start, end, duration)
Dictionary<int, List<IntervalVar>> machineToIntervals = new Dictionary<int, List<IntervalVar>>();
for (int jobID = 0; jobID < allJobs.Count(); ++jobID)
{
var job = allJobs[jobID];
for (int taskID = 0; taskID < job.Count(); ++taskID)
{
var task = job[taskID];
String suffix = $"_{jobID}_{taskID}";
IntVar start = model.NewIntVar(0, horizon, "start" + suffix);
IntVar end = model.NewIntVar(0, horizon, "end" + suffix);
IntervalVar interval = model.NewIntervalVar(start, task.duration, end, "interval" + suffix);
var key = Tuple.Create(jobID, taskID);
allTasks[key] = Tuple.Create(start, end, interval);
if (!machineToIntervals.ContainsKey(task.machine))
{
machineToIntervals.Add(task.machine, new List<IntervalVar>());
}
machineToIntervals[task.machine].Add(interval);
}
}
// Create and add disjunctive constraints.
foreach (int machine in allMachines)
{
model.AddNoOverlap(machineToIntervals[machine]);
}
// Precedences inside a job.
for (int jobID = 0; jobID < allJobs.Count(); ++jobID)
{
var job = allJobs[jobID];
for (int taskID = 0; taskID < job.Count() - 1; ++taskID)
{
var key = Tuple.Create(jobID, taskID);
var nextKey = Tuple.Create(jobID, taskID + 1);
model.Add(allTasks[nextKey].Item1 >= allTasks[key].Item2);
}
}
// Makespan objective.
IntVar objVar = model.NewIntVar(0, horizon, "makespan");
List<IntVar> ends = new List<IntVar>();
for (int jobID = 0; jobID < allJobs.Count(); ++jobID)
{
var job = allJobs[jobID];
var key = Tuple.Create(jobID, job.Count() - 1);
ends.Add(allTasks[key].Item2);
}
model.AddMaxEquality(objVar, ends);
model.Minimize(objVar);
// Solve
CpSolver solver = new CpSolver();
CpSolverStatus status = solver.Solve(model);
Console.WriteLine($"Solve status: {status}");
if (status == CpSolverStatus.Optimal || status == CpSolverStatus.Feasible)
{
Console.WriteLine("Solution:");
Dictionary<int, List<AssignedTask>> assignedJobs = new Dictionary<int, List<AssignedTask>>();
for (int jobID = 0; jobID < allJobs.Count(); ++jobID)
{
var job = allJobs[jobID];
for (int taskID = 0; taskID < job.Count(); ++taskID)
{
var task = job[taskID];
var key = Tuple.Create(jobID, taskID);
int start = (int)solver.Value(allTasks[key].Item1);
if (!assignedJobs.ContainsKey(task.machine))
{
assignedJobs.Add(task.machine, new List<AssignedTask>());
}
assignedJobs[task.machine].Add(new AssignedTask(jobID, taskID, start, task.duration));
}
}
// Create per machine output lines.
String output = "";
foreach (int machine in allMachines)
{
// Sort by starting time.
assignedJobs[machine].Sort();
String solLineTasks = $"Machine {machine}: ";
String solLine = " ";
foreach (var assignedTask in assignedJobs[machine])
{
String name = $"job_{assignedTask.jobID}_task_{assignedTask.taskID}";
// Add spaces to output to align columns.
solLineTasks += $"{name,-15}";
String solTmp = $"[{assignedTask.start},{assignedTask.start+assignedTask.duration}]";
// Add spaces to output to align columns.
solLine += $"{solTmp,-15}";
}
output += solLineTasks + "\n";
output += solLine + "\n";
}
// Finally print the solution found.
Console.WriteLine($"Optimal Schedule Length: {solver.ObjectiveValue}");
Console.WriteLine($"\n{output}");
}
else
{
Console.WriteLine("No solution found.");
}
Console.WriteLine("Statistics");
Console.WriteLine($" conflicts: {solver.NumConflicts()}");
Console.WriteLine($" branches : {solver.NumBranches()}");
Console.WriteLine($" wall time: {solver.WallTime()}s");
}
}











暂无评论内容