new course scheduling example; sync c++ examples with internal code
This commit is contained in:
843
examples/cpp/course_scheduling.cc
Normal file
843
examples/cpp/course_scheduling.cc
Normal file
@@ -0,0 +1,843 @@
|
||||
// Copyright 2010-2018 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "examples/cpp/course_scheduling.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/numbers.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "examples/cpp/course_scheduling.pb.h"
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/base/mathutil.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
#include "ortools/linear_solver/linear_solver.h"
|
||||
|
||||
namespace operations_research {
|
||||
|
||||
absl::Status CourseSchedulingSolver::ValidateModelAndLoadClasses(
|
||||
const CourseSchedulingModel& model) {
|
||||
time_slot_count_ = model.days_count() * model.daily_time_slot_count();
|
||||
room_count_ = model.rooms_size();
|
||||
solve_for_rooms_ = room_count_ > 0;
|
||||
// If there are no rooms given, we have to give room_count at least one in
|
||||
// order for the loops creating the solver variables and constraints to work.
|
||||
if (!solve_for_rooms_) {
|
||||
room_count_ = 1;
|
||||
}
|
||||
|
||||
// Validate the information given for each course.
|
||||
for (const Course& course : model.courses()) {
|
||||
if (course.consecutive_slots_count() != 1 &&
|
||||
course.consecutive_slots_count() != 2) {
|
||||
return absl::InvalidArgumentError(absl::StrFormat(
|
||||
"The course titled %s has %d consecutive time slots specified when "
|
||||
"it can only have 1 or 2.",
|
||||
course.display_name(), course.consecutive_slots_count()));
|
||||
}
|
||||
|
||||
if (course.teacher_section_counts_size() != course.teacher_indices_size()) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("The course titled %s should have the same number of "
|
||||
"teacher indices and section numbers.",
|
||||
course.display_name()));
|
||||
}
|
||||
|
||||
for (const int teacher_index : course.teacher_indices()) {
|
||||
if (teacher_index >= model.teachers_size()) {
|
||||
return absl::InvalidArgumentError(absl::StrFormat(
|
||||
"The course titled %s has teacher %d assigned to it but there are "
|
||||
"only %d teachers.",
|
||||
course.display_name(), teacher_index, model.teachers_size()));
|
||||
}
|
||||
}
|
||||
|
||||
for (const int room_index : course.room_indices()) {
|
||||
if (room_index >= model.rooms_size()) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("The course titled %s is slotted for room index %d "
|
||||
"but there are only %d rooms.",
|
||||
course.display_name(), room_index, room_count_));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the information given for each teacher and create hash sets of the
|
||||
// restricted indices for each teacher.
|
||||
teacher_to_restricted_slots_ =
|
||||
std::vector<absl::flat_hash_set<int>>(model.teachers_size());
|
||||
for (int teacher_index = 0; teacher_index < model.teachers_size();
|
||||
++teacher_index) {
|
||||
for (const int restricted_slot :
|
||||
model.teachers(teacher_index).restricted_time_slots()) {
|
||||
if (restricted_slot >= time_slot_count_) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("Teacher with name %s has restricted time slot %d "
|
||||
"but there are only %d time slots.",
|
||||
model.teachers(teacher_index).display_name(),
|
||||
restricted_slot, time_slot_count_));
|
||||
}
|
||||
teacher_to_restricted_slots_[teacher_index].insert(restricted_slot);
|
||||
}
|
||||
}
|
||||
|
||||
// Since each course can have multiple sections (classes), we need to
|
||||
// "flatten" out each course so that each of its sections gets a unique index.
|
||||
// This vector stores the information for calculating each unique index. The
|
||||
// first value is the unique index the course's sections begin at. The second
|
||||
// value is the total number of sections of that course.
|
||||
course_to_classes_ = std::vector<std::vector<int>>(model.courses_size());
|
||||
// For each teacher, store the class unique indices that they teach.
|
||||
teacher_to_classes_ =
|
||||
std::vector<absl::flat_hash_set<int>>(model.teachers_size());
|
||||
// Store course indices of courses that have a single section.
|
||||
absl::flat_hash_set<int> singleton_courses;
|
||||
int flattened_course_index = 0;
|
||||
for (int course_index = 0; course_index < model.courses_size();
|
||||
++course_index) {
|
||||
const Course& course = model.courses(course_index);
|
||||
int total_section_count = 0;
|
||||
for (int teacher = 0; teacher < course.teacher_indices_size(); ++teacher) {
|
||||
for (int section = 0; section < course.teacher_section_counts(teacher);
|
||||
++section) {
|
||||
teacher_to_classes_[course.teacher_indices(teacher)].insert(
|
||||
flattened_course_index);
|
||||
course_to_classes_[course_index].push_back(flattened_course_index);
|
||||
++flattened_course_index;
|
||||
}
|
||||
total_section_count += course.teacher_section_counts(teacher);
|
||||
}
|
||||
if (total_section_count == 1) {
|
||||
singleton_courses.insert(course_index);
|
||||
}
|
||||
}
|
||||
class_count_ = flattened_course_index;
|
||||
|
||||
// Validate the information given for each student. Store each student's
|
||||
// course pairs.
|
||||
for (const Student& student : model.students()) {
|
||||
for (const int course_index : student.course_indices()) {
|
||||
if (course_index >= model.courses_size()) {
|
||||
return absl::InvalidArgumentError(absl::StrFormat(
|
||||
"Student with name %s has course index %d but there are only %d "
|
||||
"courses.",
|
||||
student.display_name(), course_index, model.courses_size()));
|
||||
}
|
||||
}
|
||||
InsertSortedPairs(std::vector<int>(student.course_indices().begin(),
|
||||
student.course_indices().end()),
|
||||
&course_conflicts_);
|
||||
}
|
||||
|
||||
LOG(INFO) << "Number of days: " << model.days_count();
|
||||
LOG(INFO) << "Number of daily time slots: " << model.daily_time_slot_count();
|
||||
LOG(INFO) << "Total number of time slots: " << time_slot_count_;
|
||||
LOG(INFO) << "Number of courses: " << model.courses_size();
|
||||
LOG(INFO) << "Total number of classes: " << class_count_;
|
||||
LOG(INFO) << "Number of teachers: " << model.teachers_size();
|
||||
LOG(INFO) << "Number of students: " << model.students_size();
|
||||
if (solve_for_rooms_) {
|
||||
LOG(INFO) << "Number of rooms: " << model.rooms_size();
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
CourseSchedulingResult CourseSchedulingSolver::Solve(
|
||||
const CourseSchedulingModel& model) {
|
||||
CourseSchedulingResult result;
|
||||
|
||||
const auto validation_status = ValidateModelAndLoadClasses(model);
|
||||
if (!validation_status.ok()) {
|
||||
result.set_solver_status(
|
||||
CourseSchedulingResultStatus::SOLVER_MODEL_INVALID);
|
||||
result.set_message(validation_status.message());
|
||||
return result;
|
||||
}
|
||||
|
||||
ConflictPairs class_conflicts;
|
||||
result = SolveModel(model, class_conflicts);
|
||||
|
||||
if (result.solver_status() != SOLVER_FEASIBLE &&
|
||||
result.solver_status() != SOLVER_OPTIMAL) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto verifier_status = VerifyCourseSchedulingResult(model, result);
|
||||
if (!verifier_status.ok()) {
|
||||
result.set_solver_status(CourseSchedulingResultStatus::ABNORMAL);
|
||||
result.set_message(verifier_status.message());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
CourseSchedulingResult CourseSchedulingSolver::SolveModel(
|
||||
const CourseSchedulingModel& model, const ConflictPairs& class_conflicts) {
|
||||
CourseSchedulingResult result;
|
||||
|
||||
result = ScheduleCourses(class_conflicts, model);
|
||||
if (result.solver_status() != SOLVER_FEASIBLE &&
|
||||
result.solver_status() != SOLVER_OPTIMAL) {
|
||||
if (result.solver_status() == SOLVER_INFEASIBLE) {
|
||||
result.set_message("The problem is infeasible with the given courses.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ConflictPairs class_conflicts_to_try = AssignStudents(model, &result);
|
||||
|
||||
if (class_conflicts_to_try.empty()) return result;
|
||||
|
||||
std::vector<std::pair<int, int>> conflicts(class_conflicts_to_try.begin(),
|
||||
class_conflicts_to_try.end());
|
||||
|
||||
const int conflicts_count = conflicts.size();
|
||||
const int conflicts_log = conflicts_count == 1 ? 1 : log2(conflicts_count);
|
||||
for (int i = 0; i < conflicts_log; ++i) {
|
||||
const int divisions = MathUtil::IPow<double>(2, i);
|
||||
for (int j = 0; j < divisions; ++j) {
|
||||
const int start = std::floor(conflicts_count * j / divisions);
|
||||
const int end = std::floor(conflicts_count * (j + 1) / divisions);
|
||||
|
||||
ConflictPairs new_class_conflicts = class_conflicts;
|
||||
if (end >= conflicts_count) {
|
||||
new_class_conflicts.insert(conflicts.begin() + start, conflicts.end());
|
||||
} else {
|
||||
new_class_conflicts.insert(conflicts.begin() + start,
|
||||
conflicts.begin() + end);
|
||||
}
|
||||
|
||||
result = SolveModel(model, new_class_conflicts);
|
||||
if (result.solver_status() == SOLVER_FEASIBLE ||
|
||||
result.solver_status() == SOLVER_OPTIMAL) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<int> CourseSchedulingSolver::GetRoomIndices(const Course& course) {
|
||||
if (solve_for_rooms_) {
|
||||
return std::vector<int>(course.room_indices().begin(),
|
||||
course.room_indices().end());
|
||||
}
|
||||
|
||||
return {0};
|
||||
}
|
||||
|
||||
void CourseSchedulingSolver::InsertSortedPairs(const std::vector<int>& list,
|
||||
ConflictPairs* pairs) {
|
||||
for (int first = 1; first < list.size(); ++first) {
|
||||
for (int second = first; second < list.size(); ++second) {
|
||||
pairs->insert(std::minmax(list[first - 1], list[second]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<absl::flat_hash_set<int>>
|
||||
CourseSchedulingSolver::GetClassesByTimeSlot(
|
||||
const CourseSchedulingResult* result) {
|
||||
std::vector<absl::flat_hash_set<int>> time_slot_to_classes(time_slot_count_);
|
||||
|
||||
for (const ClassAssignment& class_assignment : result->class_assignments()) {
|
||||
const int course_index = class_assignment.course_index();
|
||||
const int section_number = class_assignment.section_number();
|
||||
for (const int time_slot : class_assignment.time_slots()) {
|
||||
time_slot_to_classes[time_slot].insert(
|
||||
course_to_classes_[course_index][section_number]);
|
||||
}
|
||||
}
|
||||
|
||||
return time_slot_to_classes;
|
||||
}
|
||||
|
||||
void CourseSchedulingSolver::AddVariableIfNonNull(double coeff,
|
||||
const MPVariable* var,
|
||||
MPConstraint* ct) {
|
||||
if (var == nullptr) return;
|
||||
|
||||
ct->SetCoefficient(var, coeff);
|
||||
}
|
||||
|
||||
CourseSchedulingResult CourseSchedulingSolver::ScheduleCourses(
|
||||
const ConflictPairs& class_conflicts, const CourseSchedulingModel& model) {
|
||||
LOG(INFO) << "Starting schedule courses solver with "
|
||||
<< class_conflicts.size() << " class conflicts.";
|
||||
MPSolver mip_solver("CourseSchedulingMIP",
|
||||
MPSolver::SCIP_MIXED_INTEGER_PROGRAMMING);
|
||||
const double kInfinity = std::numeric_limits<double>::infinity();
|
||||
|
||||
// Create binary variables x(n,t,r) where x(n,t,r) = 1 indicates that course n
|
||||
// is scheduled for time slot t in course r. Variables are only created if
|
||||
// attendees of class n are available for time slot t and if the course can be
|
||||
// placed into room r.
|
||||
std::vector<std::vector<std::vector<MPVariable*>>> variables(
|
||||
class_count_,
|
||||
std::vector<std::vector<MPVariable*>>(
|
||||
time_slot_count_, std::vector<MPVariable*>(room_count_, nullptr)));
|
||||
for (int proto_index = 0; proto_index < model.courses_size(); ++proto_index) {
|
||||
const Course& course = model.courses(proto_index);
|
||||
const int course_index = course_to_classes_[proto_index][0];
|
||||
int total_section_index = 0;
|
||||
for (int i = 0; i < course.teacher_section_counts_size(); ++i) {
|
||||
const int teacher = course.teacher_indices(i);
|
||||
const int section_count = course.teacher_section_counts(i);
|
||||
for (int section = 0; section < section_count; ++section) {
|
||||
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
|
||||
if (teacher_to_restricted_slots_[teacher].contains(time_slot)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const int room : GetRoomIndices(course)) {
|
||||
variables[course_index + total_section_index][time_slot][room] =
|
||||
mip_solver.MakeBoolVar(absl::StrFormat(
|
||||
"x_%d_%d_%d", course_index + total_section_index, time_slot,
|
||||
room));
|
||||
}
|
||||
}
|
||||
++total_section_index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::vector<MPVariable*>> intermediate_vars(
|
||||
class_count_, std::vector<MPVariable*>(time_slot_count_));
|
||||
for (int class_index = 0; class_index < class_count_; ++class_index) {
|
||||
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
|
||||
MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 0);
|
||||
for (int room = 0; room < room_count_; ++room) {
|
||||
if (variables[class_index][time_slot][room] == nullptr) continue;
|
||||
|
||||
ct->SetCoefficient(variables[class_index][time_slot][room], 1);
|
||||
}
|
||||
if (!ct->terms().empty()) {
|
||||
intermediate_vars[class_index][time_slot] = mip_solver.MakeBoolVar(
|
||||
absl::StrFormat("intermediate_%d_%d", class_index, time_slot));
|
||||
ct->SetCoefficient(intermediate_vars[class_index][time_slot], -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Each course meets no more than its number of consecutive time slots a
|
||||
// day.
|
||||
for (int day = 0; day < model.days_count(); ++day) {
|
||||
for (int course = 0; course < model.courses_size(); ++course) {
|
||||
const int consecutive_slot_count =
|
||||
model.courses(course).consecutive_slots_count();
|
||||
for (const int class_index : course_to_classes_[course]) {
|
||||
MPConstraint* const ct =
|
||||
mip_solver.MakeRowConstraint(0, consecutive_slot_count);
|
||||
for (int daily_time_slot = 0;
|
||||
daily_time_slot < model.daily_time_slot_count();
|
||||
++daily_time_slot) {
|
||||
AddVariableIfNonNull(
|
||||
1,
|
||||
intermediate_vars[class_index]
|
||||
[(day * model.daily_time_slot_count()) +
|
||||
daily_time_slot],
|
||||
ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Each course must meet the given number of times * its number of
|
||||
// consecutive time slots.
|
||||
for (int course = 0; course < model.courses_size(); ++course) {
|
||||
const int meeting_count = model.courses(course).meetings_count();
|
||||
const int consecutive_slot_count =
|
||||
model.courses(course).consecutive_slots_count();
|
||||
for (const int class_index : course_to_classes_[course]) {
|
||||
MPConstraint* const ct =
|
||||
mip_solver.MakeRowConstraint(meeting_count * consecutive_slot_count,
|
||||
meeting_count * consecutive_slot_count);
|
||||
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
|
||||
AddVariableIfNonNull(1, intermediate_vars[class_index][time_slot], ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Teachers are scheduled for no more than one course per time slot.
|
||||
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
|
||||
for (int teacher = 0; teacher < model.teachers_size(); ++teacher) {
|
||||
MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 1);
|
||||
for (const int class_index : teacher_to_classes_[teacher]) {
|
||||
AddVariableIfNonNull(1, intermediate_vars[class_index][time_slot], ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Each room can only be occupied by one course for each time slot.
|
||||
if (solve_for_rooms_) {
|
||||
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
|
||||
for (int room = 0; room < room_count_; ++room) {
|
||||
MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 1);
|
||||
for (int class_index = 0; class_index < class_count_; ++class_index) {
|
||||
AddVariableIfNonNull(1, variables[class_index][time_slot][room], ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Ensure each class is scheduled for the correct number of consecutive
|
||||
// time slots.
|
||||
for (int course = 0; course < model.courses_size(); ++course) {
|
||||
const int consecutive_slot_count =
|
||||
model.courses(course).consecutive_slots_count();
|
||||
if (consecutive_slot_count == 1) continue;
|
||||
for (const int class_index : course_to_classes_[course]) {
|
||||
for (int day = 0; day < model.days_count(); ++day) {
|
||||
for (int room = 0; room < room_count_; ++room) {
|
||||
// If only the previous time slot is chosen, force the current time
|
||||
// slot to be chosen.
|
||||
for (int daily_time_slot = 0;
|
||||
daily_time_slot < model.daily_time_slot_count();
|
||||
++daily_time_slot) {
|
||||
MPConstraint* const ct = mip_solver.MakeRowConstraint(0, kInfinity);
|
||||
const int time_slot_offset =
|
||||
day * model.daily_time_slot_count() + daily_time_slot;
|
||||
|
||||
if (daily_time_slot > 0) {
|
||||
AddVariableIfNonNull(
|
||||
1, variables[class_index][time_slot_offset - 1][room], ct);
|
||||
}
|
||||
AddVariableIfNonNull(
|
||||
-0.5, variables[class_index][time_slot_offset][room], ct);
|
||||
if (daily_time_slot < model.daily_time_slot_count() - 1) {
|
||||
AddVariableIfNonNull(
|
||||
0.5, variables[class_index][time_slot_offset + 1][room], ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Ensure that there are at least two sections of each course_conflicts_
|
||||
// pair that are scheduled for different time slots.
|
||||
for (const auto& conflict_pair : course_conflicts_) {
|
||||
const int course_1 = conflict_pair.first;
|
||||
const int course_2 = conflict_pair.second;
|
||||
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
|
||||
const int bound = course_to_classes_[course_1].size() +
|
||||
course_to_classes_[course_2].size() - 1;
|
||||
MPConstraint* const ct = mip_solver.MakeRowConstraint(0, bound);
|
||||
for (const int class_1 : course_to_classes_[course_1]) {
|
||||
AddVariableIfNonNull(1, intermediate_vars[class_1][time_slot], ct);
|
||||
}
|
||||
for (const int class_2 : course_to_classes_[course_2]) {
|
||||
AddVariableIfNonNull(1, intermediate_vars[class_2][time_slot], ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Ensure that conflicting class pairs are not scheduled for the same time
|
||||
// slot.
|
||||
for (const auto& conflict_pair : class_conflicts) {
|
||||
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
|
||||
MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 1);
|
||||
AddVariableIfNonNull(1, intermediate_vars[conflict_pair.first][time_slot],
|
||||
ct);
|
||||
AddVariableIfNonNull(
|
||||
1, intermediate_vars[conflict_pair.second][time_slot], ct);
|
||||
}
|
||||
}
|
||||
|
||||
MPSolver::ResultStatus status = mip_solver.Solve();
|
||||
|
||||
CourseSchedulingResult result;
|
||||
result.set_solver_status(MipStatusToCourseSchedulingResultStatus(status));
|
||||
if (status != MPSolver::OPTIMAL && status != MPSolver::FEASIBLE) {
|
||||
if (status == MPSolver::UNBOUNDED) {
|
||||
result.set_message(
|
||||
"MIP solver returned UNBOUNDED: the model is solved but the solution "
|
||||
"is infinity");
|
||||
} else if (status == MPSolver::ABNORMAL) {
|
||||
result.set_message(
|
||||
"MIP solver returned ABNORMAL: some error occurred while solving");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
for (int course = 0; course < model.courses_size(); ++course) {
|
||||
for (int section = 0; section < course_to_classes_[course].size();
|
||||
++section) {
|
||||
ClassAssignment class_assignment;
|
||||
class_assignment.set_course_index(course);
|
||||
class_assignment.set_section_number(section);
|
||||
const int class_index = course_to_classes_[course][section];
|
||||
|
||||
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
|
||||
for (int room = 0; room < room_count_; ++room) {
|
||||
MPVariable* x_i = variables[class_index][time_slot][room];
|
||||
if (x_i != nullptr && x_i->solution_value() == 1) {
|
||||
if (solve_for_rooms_) {
|
||||
class_assignment.add_room_indices(room);
|
||||
}
|
||||
class_assignment.add_time_slots(time_slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
*result.add_class_assignments() = class_assignment;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
CourseSchedulingSolver::ConflictPairs CourseSchedulingSolver::AssignStudents(
|
||||
const CourseSchedulingModel& model, CourseSchedulingResult* result) {
|
||||
LOG(INFO) << "Starting assign students solver.";
|
||||
MPSolver mip_solver("AssignStudentsMIP",
|
||||
MPSolver::SCIP_MIXED_INTEGER_PROGRAMMING);
|
||||
|
||||
// Create binary variables y(s,n) where y(s,n) = 1 indicates that student s is
|
||||
// enrolled in class n. Variables are created for a student and each section
|
||||
// of a course that they are signed up to take.
|
||||
std::vector<std::vector<MPVariable*>> variables(
|
||||
model.students_size(), std::vector<MPVariable*>(class_count_, nullptr));
|
||||
for (int student_index = 0; student_index < model.students_size();
|
||||
++student_index) {
|
||||
const Student& student = model.students(student_index);
|
||||
for (const int course_index : student.course_indices()) {
|
||||
for (const int class_index : course_to_classes_[course_index]) {
|
||||
variables[student_index][class_index] = mip_solver.MakeBoolVar(
|
||||
absl::StrFormat("y_%d_%d", student_index, class_index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Each student must be assigned to exactly one section for each course
|
||||
// they are signed up to take.
|
||||
for (int student_index = 0; student_index < model.students_size();
|
||||
++student_index) {
|
||||
const Student& student = model.students(student_index);
|
||||
for (const int course_index : student.course_indices()) {
|
||||
MPConstraint* const ct = mip_solver.MakeRowConstraint(1, 1);
|
||||
for (const int class_index : course_to_classes_[course_index]) {
|
||||
AddVariableIfNonNull(1, variables[student_index][class_index], ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Ensure that the minimum and maximum capacities for each class are met.
|
||||
for (int course_index = 0; course_index < model.courses_size();
|
||||
++course_index) {
|
||||
const Course& course = model.courses(course_index);
|
||||
const int min_cap = course.min_capacity();
|
||||
const int max_cap = course.max_capacity();
|
||||
for (const int class_index : course_to_classes_[course_index]) {
|
||||
MPConstraint* const ct = mip_solver.MakeRowConstraint(min_cap, max_cap);
|
||||
for (int student = 0; student < model.students_size(); ++student) {
|
||||
AddVariableIfNonNull(1, variables[student][class_index], ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Each student should be assigned to one class per time slot. This is a
|
||||
// soft constraint -- if violated, then the variable infeasibility_var(s,t)
|
||||
// will be greater than 0 for that student s and time slot t.
|
||||
std::vector<std::vector<MPVariable*>> infeasibility_vars(
|
||||
model.students_size(),
|
||||
std::vector<MPVariable*>(time_slot_count_, nullptr));
|
||||
std::vector<absl::flat_hash_set<int>> time_slot_to_classes =
|
||||
GetClassesByTimeSlot(result);
|
||||
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
|
||||
for (int student_index = 0; student_index < model.students_size();
|
||||
++student_index) {
|
||||
infeasibility_vars[student_index][time_slot] = mip_solver.MakeIntVar(
|
||||
0, class_count_,
|
||||
absl::StrFormat("f_%d_%d", student_index, time_slot));
|
||||
const Student& student = model.students(student_index);
|
||||
|
||||
MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 1);
|
||||
ct->SetCoefficient(infeasibility_vars[student_index][time_slot], -1);
|
||||
for (const int course_index : student.course_indices()) {
|
||||
for (const int class_index : course_to_classes_[course_index]) {
|
||||
if (!time_slot_to_classes[time_slot].contains(class_index)) continue;
|
||||
|
||||
AddVariableIfNonNull(1, variables[student_index][class_index], ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Minimize the infeasibility vars. If the objective is 0, then we have found
|
||||
// a feasible solution for the course schedule. If the objective is greater
|
||||
// than 0, then some students were assigned to multiple courses for the same
|
||||
// time slot and we need to find a new schedule for the courses.
|
||||
MPObjective* const objective = mip_solver.MutableObjective();
|
||||
for (int student_index = 0; student_index < model.students_size();
|
||||
++student_index) {
|
||||
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
|
||||
objective->SetCoefficient(infeasibility_vars[student_index][time_slot],
|
||||
1);
|
||||
}
|
||||
}
|
||||
|
||||
mip_solver.SetSolverSpecificParametersAsString("limits/gap=0.01");
|
||||
MPSolver::ResultStatus status = mip_solver.Solve();
|
||||
ConflictPairs class_conflict_pairs;
|
||||
|
||||
// This model will only be infeasible if the minimum or maximum capacities
|
||||
// are violated.
|
||||
if (status != MPSolver::OPTIMAL && status != MPSolver::FEASIBLE) {
|
||||
result->set_solver_status(MipStatusToCourseSchedulingResultStatus(status));
|
||||
result->clear_class_assignments();
|
||||
if (status == MPSolver::INFEASIBLE) {
|
||||
result->set_message(
|
||||
"Check the minimum or maximum capacity constraints for your "
|
||||
"classes.");
|
||||
}
|
||||
|
||||
return class_conflict_pairs;
|
||||
}
|
||||
|
||||
LOG(INFO) << "Finished assign students solver with " << objective->Value()
|
||||
<< " schedule violations.";
|
||||
if (objective->Value() > 0) {
|
||||
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
|
||||
for (int student_index = 0; student_index < model.students_size();
|
||||
++student_index) {
|
||||
std::vector<int> conflicting_classes;
|
||||
MPVariable* const f_i = infeasibility_vars[student_index][time_slot];
|
||||
if (f_i != nullptr && f_i->solution_value() == 0) continue;
|
||||
|
||||
for (const int class_index : time_slot_to_classes[time_slot]) {
|
||||
MPVariable* const y_i = variables[student_index][class_index];
|
||||
if (y_i != nullptr && y_i->solution_value() == 1) {
|
||||
conflicting_classes.push_back(class_index);
|
||||
}
|
||||
}
|
||||
InsertSortedPairs(conflicting_classes, &class_conflict_pairs);
|
||||
}
|
||||
}
|
||||
return class_conflict_pairs;
|
||||
}
|
||||
|
||||
for (int student_index = 0; student_index < model.students_size();
|
||||
++student_index) {
|
||||
StudentAssignment student_assignment;
|
||||
student_assignment.set_student_index(student_index);
|
||||
|
||||
const Student& student = model.students(student_index);
|
||||
for (const int course_index : student.course_indices()) {
|
||||
for (int section_index = 0;
|
||||
section_index < course_to_classes_[course_index].size();
|
||||
++section_index) {
|
||||
int class_index = course_to_classes_[course_index][section_index];
|
||||
MPVariable* const y_i = variables[student_index][class_index];
|
||||
|
||||
if (y_i->solution_value() == 1) {
|
||||
student_assignment.add_course_indices(course_index);
|
||||
student_assignment.add_section_indices(section_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
*result->add_student_assignments() = student_assignment;
|
||||
}
|
||||
|
||||
return class_conflict_pairs;
|
||||
}
|
||||
|
||||
CourseSchedulingResultStatus
|
||||
CourseSchedulingSolver::MipStatusToCourseSchedulingResultStatus(
|
||||
MPSolver::ResultStatus mip_status) {
|
||||
switch (mip_status) {
|
||||
case MPSolver::ResultStatus::OPTIMAL:
|
||||
return SOLVER_OPTIMAL;
|
||||
case MPSolver::ResultStatus::FEASIBLE:
|
||||
return SOLVER_FEASIBLE;
|
||||
case MPSolver::ResultStatus::INFEASIBLE:
|
||||
return SOLVER_INFEASIBLE;
|
||||
case MPSolver::ResultStatus::UNBOUNDED:
|
||||
case MPSolver::ResultStatus::MODEL_INVALID:
|
||||
return SOLVER_MODEL_INVALID;
|
||||
case MPSolver::ResultStatus::NOT_SOLVED:
|
||||
return SOLVER_NOT_SOLVED;
|
||||
case MPSolver::ResultStatus::ABNORMAL:
|
||||
return ABNORMAL;
|
||||
default:
|
||||
return COURSE_SCHEDULING_RESULT_STATUS_UNSPECIFIED;
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status CourseSchedulingSolver::VerifyCourseSchedulingResult(
|
||||
const CourseSchedulingModel& model, const CourseSchedulingResult& result) {
|
||||
std::vector<absl::flat_hash_set<int>> class_to_time_slots(class_count_);
|
||||
std::vector<absl::flat_hash_set<int>> room_to_time_slots(model.rooms_size());
|
||||
for (const ClassAssignment& class_assignment : result.class_assignments()) {
|
||||
const int course_index = class_assignment.course_index();
|
||||
const int meetings_count = model.courses(course_index).meetings_count();
|
||||
const int consecutive_time_slots =
|
||||
model.courses(course_index).consecutive_slots_count();
|
||||
|
||||
// Verify that each class meets the correct number of times.
|
||||
if (class_assignment.time_slots_size() !=
|
||||
meetings_count * consecutive_time_slots) {
|
||||
return absl::InvalidArgumentError(absl::StrFormat(
|
||||
"Verification failed: The course titled %s and section number %d "
|
||||
"meets %d times when it should meet %d times.",
|
||||
model.courses(course_index).display_name(),
|
||||
class_assignment.section_number(), class_assignment.time_slots_size(),
|
||||
meetings_count * consecutive_time_slots));
|
||||
}
|
||||
|
||||
const int class_index =
|
||||
course_to_classes_[course_index][class_assignment.section_number()];
|
||||
std::vector<std::vector<int>> day_to_time_slots(model.days_count());
|
||||
for (const int time_slot : class_assignment.time_slots()) {
|
||||
class_to_time_slots[class_index].insert(time_slot);
|
||||
day_to_time_slots[std::floor(time_slot / model.daily_time_slot_count())]
|
||||
.push_back(time_slot);
|
||||
}
|
||||
|
||||
// Verify that a class meets no more than its consecutive time slot count
|
||||
// per day. If a class needs 2 consecutive time slots, verify that it is
|
||||
// scheduled accordingly.
|
||||
for (int day = 0; day < model.days_count(); ++day) {
|
||||
const int day_count = day_to_time_slots[day].size();
|
||||
if (day_count != 0 && day_count != consecutive_time_slots) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("Verification failed: The course titled %s does "
|
||||
"not meet the correct number of times in "
|
||||
"day %d.",
|
||||
model.courses(course_index).display_name(), day));
|
||||
}
|
||||
if (day_count != 2) continue;
|
||||
|
||||
const int first_slot = day_to_time_slots[day][0];
|
||||
const int second_slot = day_to_time_slots[day][1];
|
||||
if (std::abs(first_slot - second_slot) != 1) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("Verification failed: The course titled %s is not "
|
||||
"scheduled for consecutive time slots "
|
||||
"in day %d.",
|
||||
model.courses(course_index).display_name(), day));
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that their is no more than 1 class per room for each time slot.
|
||||
if (solve_for_rooms_) {
|
||||
for (int i = 0; i < class_assignment.room_indices_size(); ++i) {
|
||||
const int room = class_assignment.room_indices(i);
|
||||
const int time_slot = class_assignment.time_slots(i);
|
||||
if (room_to_time_slots[room].contains(time_slot)) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("Verification failed: Multiple classes have "
|
||||
"been assigned to room %s during time slot %d.",
|
||||
model.rooms(room).display_name(), time_slot));
|
||||
}
|
||||
room_to_time_slots[room].insert(time_slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that each teacher is assigned to no more than one class per time
|
||||
// slot and that each teacher is not assigned to their restricted time slots.
|
||||
for (int teacher = 0; teacher < model.teachers_size(); ++teacher) {
|
||||
const auto& class_list = teacher_to_classes_[teacher];
|
||||
absl::flat_hash_set<int> teacher_time_slots;
|
||||
for (const int class_index : class_list) {
|
||||
for (const int time_slot : class_to_time_slots[class_index]) {
|
||||
if (teacher_to_restricted_slots_[teacher].contains(time_slot)) {
|
||||
return absl::InvalidArgumentError(absl::StrFormat(
|
||||
"Verification failed: Teacher with name %s has been assigned to "
|
||||
"restricted time slot %d.",
|
||||
model.teachers(teacher).display_name(), time_slot));
|
||||
}
|
||||
if (teacher_time_slots.contains(time_slot)) {
|
||||
return absl::InvalidArgumentError(absl::StrFormat(
|
||||
"Verification failed: Teacher with name %s has been assigned to "
|
||||
"multiple classes at time slot %d.",
|
||||
model.teachers(teacher).display_name(), time_slot));
|
||||
}
|
||||
teacher_time_slots.insert(time_slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<int> class_student_count(class_count_);
|
||||
for (const StudentAssignment& student_assignment :
|
||||
result.student_assignments()) {
|
||||
const int student_index = student_assignment.student_index();
|
||||
|
||||
// Verify that each student is assigned to the correct courses.
|
||||
std::vector<int> enrolled_courses =
|
||||
std::vector<int>(model.students(student_index).course_indices().begin(),
|
||||
model.students(student_index).course_indices().end());
|
||||
std::vector<int> assigned_courses =
|
||||
std::vector<int>(student_assignment.course_indices().begin(),
|
||||
student_assignment.course_indices().end());
|
||||
std::sort(enrolled_courses.begin(), enrolled_courses.end());
|
||||
std::sort(assigned_courses.begin(), assigned_courses.end());
|
||||
if (enrolled_courses != assigned_courses) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("Verification failed: Student with name %s has not "
|
||||
"been assigned the correct courses.",
|
||||
model.students(student_index).display_name()));
|
||||
}
|
||||
|
||||
// Verify that each student is assigned to no more than one class per time
|
||||
// slot.
|
||||
absl::flat_hash_set<int> student_time_slots;
|
||||
for (int i = 0; i < student_assignment.course_indices_size(); ++i) {
|
||||
const int course_index = student_assignment.course_indices(i);
|
||||
const int section = student_assignment.section_indices(i);
|
||||
const int class_index = course_to_classes_[course_index][section];
|
||||
++class_student_count[class_index];
|
||||
|
||||
for (const int time_slot : class_to_time_slots[class_index]) {
|
||||
if (student_time_slots.contains(time_slot)) {
|
||||
return absl::InvalidArgumentError(absl::StrFormat(
|
||||
"Verification failed: Student with name %s has been assigned to "
|
||||
"multiple classes at time slot %d.",
|
||||
model.students(student_index).display_name(), time_slot));
|
||||
}
|
||||
student_time_slots.insert(time_slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify size of each class is within the minimum and maximum capacities.
|
||||
for (int course = 0; course < model.courses_size(); ++course) {
|
||||
const int min_cap = model.courses(course).min_capacity();
|
||||
const int max_cap = model.courses(course).max_capacity();
|
||||
for (const int class_index : course_to_classes_[course]) {
|
||||
const int class_size = class_student_count[class_index];
|
||||
if (class_size < min_cap) {
|
||||
return absl::InvalidArgumentError(absl::StrFormat(
|
||||
"Verification failed: The course titled %s has %d students when it "
|
||||
"should have at least %d students.",
|
||||
model.courses(course).display_name(), class_size, min_cap));
|
||||
}
|
||||
if (class_size > max_cap) {
|
||||
return absl::InvalidArgumentError(absl::StrFormat(
|
||||
"Verification failed: The course titled %s has %d students when it "
|
||||
"should have no more than %d students.",
|
||||
model.courses(course).display_name(), class_size, max_cap));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace operations_research
|
||||
86
examples/cpp/course_scheduling.h
Normal file
86
examples/cpp/course_scheduling.h
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright 2010-2018 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef OR_TOOLS_EXAMPLES_COURSE_SCHEDULING_H_
|
||||
#define OR_TOOLS_EXAMPLES_COURSE_SCHEDULING_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "examples/cpp/course_scheduling.pb.h"
|
||||
#include "ortools/linear_solver/linear_solver.h"
|
||||
#include "ortools/sat/cp_model.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
class CourseSchedulingSolver {
|
||||
public:
|
||||
CourseSchedulingSolver() : solve_for_rooms_(false) {}
|
||||
virtual ~CourseSchedulingSolver() {}
|
||||
|
||||
using ConflictPairs = absl::flat_hash_set<std::pair<int, int>>;
|
||||
|
||||
CourseSchedulingResult Solve(const CourseSchedulingModel& model);
|
||||
|
||||
protected:
|
||||
virtual absl::Status ValidateModelAndLoadClasses(
|
||||
const CourseSchedulingModel& model);
|
||||
|
||||
virtual CourseSchedulingResult SolveModel(
|
||||
const CourseSchedulingModel& model, const ConflictPairs& class_conflicts);
|
||||
|
||||
virtual absl::Status VerifyCourseSchedulingResult(
|
||||
const CourseSchedulingModel& model, const CourseSchedulingResult& result);
|
||||
|
||||
private:
|
||||
CourseSchedulingResult ScheduleCourses(const ConflictPairs& class_conflicts,
|
||||
const CourseSchedulingModel& model);
|
||||
|
||||
// This method will modify the CourseSchedulingResult returned from
|
||||
// ScheduleCoursesMip, which is why the result is passed in as a pointer.
|
||||
ConflictPairs AssignStudents(const CourseSchedulingModel& model,
|
||||
CourseSchedulingResult* result);
|
||||
|
||||
int GetTeacherIndex(int course_index, int section);
|
||||
|
||||
void InsertSortedPairs(const std::vector<int>& list, ConflictPairs* pairs);
|
||||
|
||||
bool ShouldCreateVariable(int course_index, int section, int time_slot,
|
||||
int room);
|
||||
|
||||
std::vector<int> GetRoomIndices(const Course& course);
|
||||
|
||||
std::vector<absl::flat_hash_set<int>> GetClassesByTimeSlot(
|
||||
const CourseSchedulingResult* result);
|
||||
|
||||
void AddVariableIfNonNull(double coeff, const MPVariable* var,
|
||||
MPConstraint* ct);
|
||||
|
||||
CourseSchedulingResultStatus MipStatusToCourseSchedulingResultStatus(
|
||||
MPSolver::ResultStatus mip_status);
|
||||
|
||||
bool solve_for_rooms_;
|
||||
int class_count_;
|
||||
int time_slot_count_;
|
||||
int room_count_;
|
||||
ConflictPairs course_conflicts_;
|
||||
std::vector<absl::flat_hash_set<int>> teacher_to_classes_;
|
||||
std::vector<absl::flat_hash_set<int>> teacher_to_restricted_slots_;
|
||||
std::vector<std::vector<int>> course_to_classes_;
|
||||
};
|
||||
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_EXAMPLES_COURSE_SCHEDULING_H_
|
||||
191
examples/cpp/course_scheduling.proto
Normal file
191
examples/cpp/course_scheduling.proto
Normal file
@@ -0,0 +1,191 @@
|
||||
// Copyright 2010-2018 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package operations_research;
|
||||
|
||||
import "google/api/field_behavior.proto";
|
||||
|
||||
// Information required to create a schedule for a school system.
|
||||
message CourseSchedulingModel {
|
||||
// Schedule name, used only for logging purposes.
|
||||
string display_name = 1;
|
||||
|
||||
// The number of days in a schedule rotation. If the school system uses a
|
||||
// block schedule, this value should be 1.
|
||||
int32 days_count = 2 [(google.api.field_behavior) = REQUIRED];
|
||||
|
||||
// The number of time slots each day in a schedule rotation. If the school
|
||||
// system uses a block schedule, this value is the number of blocks.
|
||||
int32 daily_time_slot_count = 3 [(google.api.field_behavior) = REQUIRED];
|
||||
|
||||
// List of courses that need to be scheduled.
|
||||
repeated Course courses = 4;
|
||||
|
||||
// List of teachers.
|
||||
repeated Teacher teachers = 5;
|
||||
|
||||
// List of students that need to be assigned to these courses.
|
||||
repeated Student students = 6;
|
||||
|
||||
// List of rooms that the courses can be assigned to.
|
||||
repeated Room rooms = 7;
|
||||
}
|
||||
|
||||
// Holds the solution to the course scheduling problem.
|
||||
message CourseSchedulingResult {
|
||||
// Human readable message about the solver or given model.
|
||||
string message = 1;
|
||||
|
||||
// Status of the solver.
|
||||
CourseSchedulingResultStatus solver_status = 2
|
||||
[(google.api.field_behavior) = REQUIRED];
|
||||
|
||||
// List of the time slot and room assignments for each section of a course.
|
||||
repeated ClassAssignment class_assignments = 3;
|
||||
|
||||
// List of course and section assignments for each student.
|
||||
repeated StudentAssignment student_assignments = 4;
|
||||
}
|
||||
|
||||
message ClassAssignment {
|
||||
// Index of the course in the CourseSchedulingModel.
|
||||
int32 course_index = 1 [(google.api.field_behavior) = REQUIRED];
|
||||
|
||||
// Specific section of the course in the CourseSchedulingModel.
|
||||
int32 section_number = 2 [(google.api.field_behavior) = REQUIRED];
|
||||
|
||||
// Time slots that this class has been assigned to in the
|
||||
// CourseSchedulingModel.
|
||||
repeated int32 time_slots = 3 [(google.api.field_behavior) = REQUIRED];
|
||||
|
||||
// Indices of the rooms that the class is assigned to in the
|
||||
// CourseSchedulingModel. If this is not empty, then the number of indices
|
||||
// must match the number of time slots.
|
||||
repeated int32 room_indices = 4;
|
||||
}
|
||||
|
||||
message StudentAssignment {
|
||||
// Index of the student in the CourseSchedulingModel.
|
||||
int32 student_index = 1 [(google.api.field_behavior) = REQUIRED];
|
||||
|
||||
// Course indices in the CourseSchedulingModel that this student has been
|
||||
// assigned to. The number of indices must match the number of section
|
||||
// indices.
|
||||
repeated int32 course_indices = 2 [(google.api.field_behavior) = REQUIRED];
|
||||
|
||||
// Section indices for each Course in the CourseSchedulingModel this this
|
||||
// student has been assigned to. The number of indices must match the number
|
||||
// of course indices.
|
||||
repeated int32 section_indices = 3 [(google.api.field_behavior) = REQUIRED];
|
||||
}
|
||||
|
||||
message Course {
|
||||
// Course name, used only for logging purposes.
|
||||
string display_name = 1;
|
||||
|
||||
// The number of times each section of this course needs to meet during a
|
||||
// schedule rotation. Each section of the course meets no more than once a
|
||||
// day. If the school system uses a block schedule, then this value should
|
||||
// be 1.
|
||||
int32 meetings_count = 2 [(google.api.field_behavior) = REQUIRED];
|
||||
|
||||
// The maximum number of students for this course. This value can be equal to
|
||||
// +Infinity to encode a course has no maximum capacity.
|
||||
int32 max_capacity = 3 [(google.api.field_behavior) = REQUIRED];
|
||||
|
||||
// The minimum number of students for this course.
|
||||
int32 min_capacity = 4;
|
||||
|
||||
// The number of consecutive time slots that each section of this course needs
|
||||
// to be scheduled for. This value can only be 1 or 2. If the value is 2, then
|
||||
// 2 consecutive time slots in a day counts as 1 meeting time for the section.
|
||||
int32 consecutive_slots_count = 5 [(google.api.field_behavior) = REQUIRED];
|
||||
|
||||
// List of indices for the teachers of this course. We are assuming that each
|
||||
// teacher teaches separately. Must have the same number of elements as the
|
||||
// number of sections list.
|
||||
repeated int32 teacher_indices = 6;
|
||||
|
||||
// The number of sections each teacher teaches of this course. Must have the
|
||||
// same number of elements as the teacher index list.
|
||||
repeated int32 teacher_section_counts = 7;
|
||||
|
||||
// List of the possible rooms that this course can be assigned to. This can
|
||||
// be empty.
|
||||
repeated int32 room_indices = 8;
|
||||
}
|
||||
|
||||
message Teacher {
|
||||
// Teacher name, used only for logging purposes.
|
||||
string display_name = 1;
|
||||
|
||||
// List of time slots that the teacher cannot be scheduled for. These time
|
||||
// slot values index to the accumulative number of time slots starting at 0.
|
||||
// For example, if a schedule rotation has 5 days and 8 time slots per day,
|
||||
// and a teacher cannot be scheduled for the last time slot of the fourth
|
||||
// day, the number here would be 31.
|
||||
repeated int32 restricted_time_slots = 2;
|
||||
}
|
||||
|
||||
message Student {
|
||||
// Student name, used only for logging purposes.
|
||||
string display_name = 1;
|
||||
|
||||
// List of course indices that the student needs to be enrolled in.
|
||||
repeated int32 course_indices = 2;
|
||||
}
|
||||
|
||||
message Room {
|
||||
// Room name, used only for logging purposes.
|
||||
string display_name = 1;
|
||||
|
||||
// Maximum number of students that can fit into this room.
|
||||
int32 capacity = 2 [(google.api.field_behavior) = REQUIRED];
|
||||
}
|
||||
|
||||
// Status returned by the solver.
|
||||
enum CourseSchedulingResultStatus {
|
||||
COURSE_SCHEDULING_RESULT_STATUS_UNSPECIFIED = 0;
|
||||
|
||||
// The solver had enough time to find some solution that satisfies all
|
||||
// constraints, but it did not prove optimality (which means it may or may
|
||||
// not have reached the optimal).
|
||||
//
|
||||
// This can happen for large LP models (linear programming), and is a frequent
|
||||
// response for time-limited MIPs (mixed integer programming). This is also
|
||||
// what the CP (constraint programming) solver will return if there is no
|
||||
// objective specified.
|
||||
SOLVER_FEASIBLE = 1;
|
||||
|
||||
// The solver found the proven optimal solution.
|
||||
SOLVER_OPTIMAL = 2;
|
||||
|
||||
// The model does not have any solution, according to the solver (which
|
||||
// "proved" it, with the caveat that numerical proofs aren't actual proofs),
|
||||
// or based on trivial considerations (eg. a variable whose lower bound is
|
||||
// strictly greater than its upper bound).
|
||||
SOLVER_INFEASIBLE = 3;
|
||||
|
||||
// Model errors. These are always deterministic and repeatable.
|
||||
// They should be accompanied with a string description of the error.
|
||||
SOLVER_MODEL_INVALID = 4;
|
||||
|
||||
// The model has not been solved in the given time or the solver was not able
|
||||
// to solve the model given.
|
||||
SOLVER_NOT_SOLVED = 5;
|
||||
|
||||
// An error (either numerical or from a bug in the code) occurred.
|
||||
ABNORMAL = 6;
|
||||
}
|
||||
108
examples/cpp/course_scheduling_run.cc
Normal file
108
examples/cpp/course_scheduling_run.cc
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright 2010-2018 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This file implements the main function for the Course Scheduling solver. It
|
||||
// reads the problem specification from an input file specified via command-line
|
||||
// flags, and prints the time slots for each course. See go/or-course-scheduling
|
||||
// for more information.
|
||||
//
|
||||
// Example usage:
|
||||
// ./course_scheduling_run --input_file=testdata/my_input_proto.textproto
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "examples/cpp/course_scheduling.h"
|
||||
#include "examples/cpp/course_scheduling.pb.h"
|
||||
#include "ortools/base/commandlineflags.h"
|
||||
#include "ortools/base/timer.h"
|
||||
|
||||
ABSL_FLAG(std::string, input, "",
|
||||
"Input file containing a CourseSchedulingModel in text format.");
|
||||
|
||||
namespace operations_research {
|
||||
|
||||
void Main() {
|
||||
CourseSchedulingModel input;
|
||||
const auto proto_status =
|
||||
file::GetTextProto(absl::GetFlag(FLAGS_input), &input, file::Defaults());
|
||||
if (!proto_status.ok()) {
|
||||
LOG(ERROR) << proto_status.message();
|
||||
return;
|
||||
}
|
||||
|
||||
CourseSchedulingSolver solver;
|
||||
WallTimer timer;
|
||||
timer.Start();
|
||||
const CourseSchedulingResult result = solver.Solve(input);
|
||||
timer.Stop();
|
||||
|
||||
LOG(INFO) << "Solver result status: "
|
||||
<< CourseSchedulingResultStatus_Name(result.solver_status()) << ". "
|
||||
<< result.message();
|
||||
|
||||
for (const ClassAssignment& class_assignment : result.class_assignments()) {
|
||||
const int course_index = class_assignment.course_index();
|
||||
const int section_number = class_assignment.section_number();
|
||||
|
||||
int teacher_index = 0;
|
||||
const Course& course = input.courses(course_index);
|
||||
int sections = 0;
|
||||
for (int section_index = 0;
|
||||
section_index < course.teacher_section_counts_size();
|
||||
++section_index) {
|
||||
sections += course.teacher_section_counts(section_index);
|
||||
if (section_number < sections) {
|
||||
teacher_index = course.teacher_indices(section_index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LOG(INFO) << course.display_name();
|
||||
LOG(INFO) << " Section: " << section_number;
|
||||
LOG(INFO) << " Teacher: " << input.teachers(teacher_index).display_name();
|
||||
for (int i = 0; i < class_assignment.time_slots_size(); ++i) {
|
||||
if (input.rooms_size() > 0) {
|
||||
LOG(INFO)
|
||||
<< " Scheduled for time slot " << class_assignment.time_slots(i)
|
||||
<< " in room "
|
||||
<< input.rooms(class_assignment.room_indices(i)).display_name();
|
||||
} else {
|
||||
LOG(INFO) << " Scheduled for time slot "
|
||||
<< class_assignment.time_slots(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const StudentAssignment& student_assignment :
|
||||
result.student_assignments()) {
|
||||
const int student_index = student_assignment.student_index();
|
||||
|
||||
LOG(INFO) << input.students(student_index).display_name();
|
||||
for (int i = 0; i < student_assignment.course_indices_size(); ++i) {
|
||||
LOG(INFO)
|
||||
<< " "
|
||||
<< input.courses(student_assignment.course_indices(i)).display_name()
|
||||
<< " " << student_assignment.section_indices(i);
|
||||
}
|
||||
}
|
||||
|
||||
LOG(INFO) << "Solved model in " << timer.GetDuration();
|
||||
}
|
||||
|
||||
} // namespace operations_research
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
absl::ParseCommandLine(argc, argv);
|
||||
operations_research::Main();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "absl/random/random.h"
|
||||
#include "examples/cpp/cvrptw_lib.h"
|
||||
#include "google/protobuf/text_format.h"
|
||||
#include "ortools/base/commandlineflags.h"
|
||||
@@ -34,7 +35,6 @@
|
||||
#include "ortools/constraint_solver/routing_parameters.h"
|
||||
#include "ortools/constraint_solver/routing_parameters.pb.h"
|
||||
|
||||
using operations_research::ACMRandom;
|
||||
using operations_research::Assignment;
|
||||
using operations_research::DefaultRoutingSearchParameters;
|
||||
using operations_research::GetSeed;
|
||||
@@ -73,8 +73,7 @@ int main(int argc, char** argv) {
|
||||
<< "Specify a non-null vehicle fleet size.";
|
||||
// VRP of size absl::GetFlag(FLAGS_vrp_size).
|
||||
// Nodes are indexed from 0 to absl::GetFlag(FLAGS_vrp_orders), the starts and
|
||||
// ends of
|
||||
// the routes are at node 0.
|
||||
// ends of the routes are at node 0.
|
||||
const RoutingIndexManager::NodeIndex kDepot(0);
|
||||
RoutingIndexManager manager(absl::GetFlag(FLAGS_vrp_orders) + 1,
|
||||
absl::GetFlag(FLAGS_vrp_vehicles), kDepot);
|
||||
@@ -109,8 +108,8 @@ int main(int argc, char** argv) {
|
||||
routing.RegisterTransitCallback([&demand, &manager](int64 i, int64 j) {
|
||||
return demand.Demand(manager.IndexToNode(i), manager.IndexToNode(j));
|
||||
}),
|
||||
kNullCapacitySlack, kVehicleCapacity, /*fix_start_cumul_to_zero=*/true,
|
||||
kCapacity);
|
||||
kNullCapacitySlack, kVehicleCapacity,
|
||||
/*fix_start_cumul_to_zero=*/true, kCapacity);
|
||||
|
||||
// Adding time dimension constraints.
|
||||
const int64 kTimePerDemandUnit = 300;
|
||||
@@ -132,12 +131,12 @@ int main(int argc, char** argv) {
|
||||
|
||||
// Adding disjoint time windows.
|
||||
Solver* solver = routing.solver();
|
||||
ACMRandom randomizer(
|
||||
std::mt19937 randomizer(
|
||||
GetSeed(absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)));
|
||||
for (int order = 1; order < manager.num_nodes(); ++order) {
|
||||
std::vector<int64> forbid_points(2 * absl::GetFlag(FLAGS_vrp_windows), 0);
|
||||
for (int i = 0; i < forbid_points.size(); ++i) {
|
||||
forbid_points[i] = randomizer.Uniform(kHorizon);
|
||||
forbid_points[i] = absl::Uniform<int32_t>(randomizer, 0, kHorizon);
|
||||
}
|
||||
std::sort(forbid_points.begin(), forbid_points.end());
|
||||
std::vector<int64> forbid_starts(1, 0);
|
||||
|
||||
@@ -23,18 +23,17 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "absl/random/random.h"
|
||||
#include "examples/cpp/cvrptw_lib.h"
|
||||
#include "google/protobuf/text_format.h"
|
||||
#include "ortools/base/commandlineflags.h"
|
||||
#include "ortools/base/integral_types.h"
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/base/random.h"
|
||||
#include "ortools/constraint_solver/routing.h"
|
||||
#include "ortools/constraint_solver/routing_index_manager.h"
|
||||
#include "ortools/constraint_solver/routing_parameters.h"
|
||||
#include "ortools/constraint_solver/routing_parameters.pb.h"
|
||||
|
||||
using operations_research::ACMRandom;
|
||||
using operations_research::Assignment;
|
||||
using operations_research::DefaultRoutingSearchParameters;
|
||||
using operations_research::GetSeed;
|
||||
@@ -71,8 +70,7 @@ int main(int argc, char** argv) {
|
||||
<< "Specify a non-null vehicle fleet size.";
|
||||
// VRP of size absl::GetFlag(FLAGS_vrp_size).
|
||||
// Nodes are indexed from 0 to absl::GetFlag(FLAGS_vrp_orders), the starts and
|
||||
// ends of
|
||||
// the routes are at node 0.
|
||||
// ends of the routes are at node 0.
|
||||
const RoutingIndexManager::NodeIndex kDepot(0);
|
||||
RoutingIndexManager manager(absl::GetFlag(FLAGS_vrp_orders) + 1,
|
||||
absl::GetFlag(FLAGS_vrp_vehicles), kDepot);
|
||||
@@ -107,8 +105,8 @@ int main(int argc, char** argv) {
|
||||
routing.RegisterTransitCallback([&demand, &manager](int64 i, int64 j) {
|
||||
return demand.Demand(manager.IndexToNode(i), manager.IndexToNode(j));
|
||||
}),
|
||||
kNullCapacitySlack, kVehicleCapacity, /*fix_start_cumul_to_zero=*/true,
|
||||
kCapacity);
|
||||
kNullCapacitySlack, kVehicleCapacity,
|
||||
/*fix_start_cumul_to_zero=*/true, kCapacity);
|
||||
|
||||
// Adding time dimension constraints.
|
||||
const int64 kTimePerDemandUnit = 300;
|
||||
@@ -129,11 +127,12 @@ int main(int argc, char** argv) {
|
||||
const RoutingDimension& time_dimension = routing.GetDimensionOrDie(kTime);
|
||||
|
||||
// Adding time windows.
|
||||
ACMRandom randomizer(
|
||||
std::mt19937 randomizer(
|
||||
GetSeed(absl::GetFlag(FLAGS_vrp_use_deterministic_random_seed)));
|
||||
const int64 kTWDuration = 5 * 3600;
|
||||
for (int order = 1; order < manager.num_nodes(); ++order) {
|
||||
const int64 start = randomizer.Uniform(kHorizon - kTWDuration);
|
||||
const int64 start =
|
||||
absl::Uniform<int32_t>(randomizer, 0, kHorizon - kTWDuration);
|
||||
time_dimension.CumulVar(order)->SetRange(start, start + kTWDuration);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/flags/flag.h"
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "google/protobuf/text_format.h"
|
||||
#include "google/protobuf/wrappers.pb.h"
|
||||
#include "ortools/base/commandlineflags.h"
|
||||
@@ -23,6 +25,7 @@
|
||||
#include "ortools/base/timer.h"
|
||||
#include "ortools/data/jobshop_scheduling.pb.h"
|
||||
#include "ortools/data/jobshop_scheduling_parser.h"
|
||||
#include "ortools/graph/connected_components.h"
|
||||
#include "ortools/sat/cp_model.h"
|
||||
#include "ortools/sat/cp_model.pb.h"
|
||||
#include "ortools/sat/model.h"
|
||||
@@ -32,8 +35,20 @@ ABSL_FLAG(std::string, params, "", "Sat parameters in text proto format.");
|
||||
ABSL_FLAG(bool, use_optional_variables, true,
|
||||
"Whether we use optional variables for bounds of an optional "
|
||||
"interval or not.");
|
||||
ABSL_FLAG(bool, use_interval_makespan, true,
|
||||
"Whether we encode the makespan using an interval or not.");
|
||||
ABSL_FLAG(bool, use_expanded_precedences, false,
|
||||
"Whether we add precedences between alternative tasks within the "
|
||||
"same job.");
|
||||
ABSL_FLAG(
|
||||
bool, use_cumulative_relaxation, true,
|
||||
"Whether we regroup multiple machines to create a cumulative relaxation.");
|
||||
ABSL_FLAG(
|
||||
int, job_suffix_relaxation_length, 5,
|
||||
"The maximum length of the suffix of a job used in the linear relaxation.");
|
||||
ABSL_FLAG(bool, display_model, false, "Display jobshop proto before solving.");
|
||||
ABSL_FLAG(bool, display_sat_model, false, "Display sat proto before solving.");
|
||||
ABSL_FLAG(int, horizon, -1, "Override horizon computation.");
|
||||
|
||||
using operations_research::data::jssp::Job;
|
||||
using operations_research::data::jssp::JobPrecedence;
|
||||
@@ -88,193 +103,262 @@ int64 ComputeHorizon(const JsspInputProblem& problem) {
|
||||
// TODO(user): Uses transitions.
|
||||
}
|
||||
|
||||
// Solve a JobShop scheduling problem using SAT.
|
||||
void Solve(const JsspInputProblem& problem) {
|
||||
if (absl::GetFlag(FLAGS_display_model)) {
|
||||
LOG(INFO) << problem.DebugString();
|
||||
}
|
||||
// A job is a sequence of tasks. For each task, we store the main interval, as
|
||||
// well as its start, size, and end variables.
|
||||
struct JobTaskData {
|
||||
IntervalVar interval;
|
||||
IntVar start;
|
||||
IntVar duration;
|
||||
IntVar end;
|
||||
};
|
||||
|
||||
CpModelBuilder cp_model;
|
||||
// Each task in a job can have multiple alternative ways of being performed.
|
||||
// This structure stores the start, end, and presence variables attached to one
|
||||
// alternative for a given task.
|
||||
struct AlternativeTaskData {
|
||||
IntervalVar interval;
|
||||
IntVar start;
|
||||
IntVar end;
|
||||
BoolVar presence;
|
||||
};
|
||||
|
||||
// Create the job structure as a chain of tasks. Fills in the job_to_tasks
|
||||
// vector.
|
||||
void CreateJobs(const JsspInputProblem& problem, int64 horizon,
|
||||
std::vector<std::vector<JobTaskData>>& job_to_tasks,
|
||||
bool& has_variable_duration_tasks, CpModelBuilder& cp_model) {
|
||||
const int num_jobs = problem.jobs_size();
|
||||
const int num_machines = problem.machines_size();
|
||||
const int64 horizon = ComputeHorizon(problem);
|
||||
|
||||
std::vector<int> starts;
|
||||
std::vector<int> ends;
|
||||
|
||||
const Domain all_horizon(0, horizon);
|
||||
|
||||
const IntVar makespan = cp_model.NewIntVar(all_horizon);
|
||||
|
||||
std::vector<std::vector<IntervalVar> > machine_to_intervals(num_machines);
|
||||
std::vector<std::vector<int> > machine_to_jobs(num_machines);
|
||||
std::vector<std::vector<IntVar> > machine_to_starts(num_machines);
|
||||
std::vector<std::vector<IntVar> > machine_to_ends(num_machines);
|
||||
std::vector<std::vector<BoolVar> > machine_to_presences(num_machines);
|
||||
std::vector<IntVar> job_starts(num_jobs);
|
||||
std::vector<IntVar> job_ends(num_jobs);
|
||||
std::vector<IntVar> task_starts;
|
||||
int64 objective_offset = 0;
|
||||
std::vector<IntVar> objective_vars;
|
||||
std::vector<int64> objective_coeffs;
|
||||
|
||||
for (int j = 0; j < num_jobs; ++j) {
|
||||
const Job& job = problem.jobs(j);
|
||||
IntVar previous_end;
|
||||
const int num_tasks_in_job = job.tasks_size();
|
||||
std::vector<JobTaskData>& task_data = job_to_tasks[j];
|
||||
|
||||
const int64 hard_start =
|
||||
job.has_earliest_start() ? job.earliest_start().value() : 0L;
|
||||
const int64 hard_end =
|
||||
job.has_latest_end() ? job.latest_end().value() : horizon;
|
||||
|
||||
for (int t = 0; t < job.tasks_size(); ++t) {
|
||||
for (int t = 0; t < num_tasks_in_job; ++t) {
|
||||
const Task& task = job.tasks(t);
|
||||
const int num_alternatives = task.machine_size();
|
||||
CHECK_EQ(num_alternatives, task.duration_size());
|
||||
|
||||
// Add the "main" task interval. It will englobe all the alternative ones
|
||||
// if there is many, or be a normal task otherwise.
|
||||
// Add the "main" task interval.
|
||||
std::vector<int64> durations;
|
||||
int64 min_duration = task.duration(0);
|
||||
int64 max_duration = task.duration(0);
|
||||
for (int i = 1; i < num_alternatives; ++i) {
|
||||
min_duration = std::min(min_duration, task.duration(i));
|
||||
max_duration = std::max(max_duration, task.duration(i));
|
||||
durations.push_back(task.duration(0));
|
||||
for (int a = 1; a < num_alternatives; ++a) {
|
||||
min_duration = std::min(min_duration, task.duration(a));
|
||||
max_duration = std::max(max_duration, task.duration(a));
|
||||
durations.push_back(task.duration(a));
|
||||
}
|
||||
|
||||
if (min_duration != max_duration) has_variable_duration_tasks = true;
|
||||
|
||||
const IntVar start = cp_model.NewIntVar(Domain(hard_start, hard_end));
|
||||
const IntVar duration =
|
||||
cp_model.NewIntVar(Domain(min_duration, max_duration));
|
||||
const IntVar duration = cp_model.NewIntVar(Domain::FromValues(durations));
|
||||
const IntVar end = cp_model.NewIntVar(Domain(hard_start, hard_end));
|
||||
const IntervalVar interval =
|
||||
cp_model.NewIntervalVar(start, duration, end);
|
||||
|
||||
// Store starts and ends of jobs for precedences.
|
||||
if (t == 0) {
|
||||
job_starts[j] = start;
|
||||
}
|
||||
if (t == job.tasks_size() - 1) {
|
||||
job_ends[j] = end;
|
||||
}
|
||||
task_starts.push_back(start);
|
||||
// Fill in job_to_tasks.
|
||||
task_data.push_back({interval, start, duration, end});
|
||||
|
||||
// Chain the task belonging to the same job.
|
||||
if (t > 0) {
|
||||
cp_model.AddLessOrEqual(previous_end, start);
|
||||
cp_model.AddLessOrEqual(task_data[t - 1].end, task_data[t].start);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For each task of each jobs, create the alternative tasks and link them to the
|
||||
// main task of the job.
|
||||
void CreateAlternativeTasks(
|
||||
const JsspInputProblem& problem,
|
||||
const std::vector<std::vector<JobTaskData>>& job_to_tasks, int64 horizon,
|
||||
std::vector<std::vector<std::vector<AlternativeTaskData>>>&
|
||||
job_task_to_alternatives,
|
||||
CpModelBuilder& cp_model) {
|
||||
const int num_jobs = problem.jobs_size();
|
||||
const BoolVar true_var = cp_model.TrueVar();
|
||||
|
||||
for (int j = 0; j < num_jobs; ++j) {
|
||||
const Job& job = problem.jobs(j);
|
||||
const int num_tasks_in_job = job.tasks_size();
|
||||
job_task_to_alternatives[j].resize(num_tasks_in_job);
|
||||
const std::vector<JobTaskData>& tasks = job_to_tasks[j];
|
||||
|
||||
const int64 hard_start =
|
||||
job.has_earliest_start() ? job.earliest_start().value() : 0L;
|
||||
const int64 hard_end =
|
||||
job.has_latest_end() ? job.latest_end().value() : horizon;
|
||||
|
||||
for (int t = 0; t < num_tasks_in_job; ++t) {
|
||||
const Task& task = job.tasks(t);
|
||||
const int num_alternatives = task.machine_size();
|
||||
CHECK_EQ(num_alternatives, task.duration_size());
|
||||
std::vector<AlternativeTaskData>& alt_data =
|
||||
job_task_to_alternatives[j][t];
|
||||
|
||||
absl::flat_hash_map<int64, std::vector<int>> duration_supports;
|
||||
duration_supports[task.duration(0)].push_back(0);
|
||||
for (int a = 1; a < num_alternatives; ++a) {
|
||||
duration_supports[task.duration(a)].push_back(a);
|
||||
}
|
||||
previous_end = end;
|
||||
|
||||
if (num_alternatives == 1) {
|
||||
const int m = task.machine(0);
|
||||
machine_to_intervals[m].push_back(interval);
|
||||
machine_to_jobs[m].push_back(j);
|
||||
machine_to_starts[m].push_back(start);
|
||||
machine_to_ends[m].push_back(end);
|
||||
machine_to_presences[m].push_back(cp_model.TrueVar());
|
||||
if (task.cost_size() > 0) {
|
||||
objective_offset += task.cost(0);
|
||||
}
|
||||
alt_data.push_back(
|
||||
{tasks[t].interval, tasks[t].start, tasks[t].end, true_var});
|
||||
} else {
|
||||
std::vector<BoolVar> presences;
|
||||
for (int a = 0; a < num_alternatives; ++a) {
|
||||
const BoolVar presence = cp_model.NewBoolVar();
|
||||
const BoolVar local_presence = cp_model.NewBoolVar();
|
||||
const IntVar local_start =
|
||||
absl::GetFlag(FLAGS_use_optional_variables)
|
||||
? cp_model.NewIntVar(Domain(hard_start, hard_end))
|
||||
: start;
|
||||
: tasks[t].start;
|
||||
const IntVar local_duration = cp_model.NewConstant(task.duration(a));
|
||||
const IntVar local_end =
|
||||
absl::GetFlag(FLAGS_use_optional_variables)
|
||||
? cp_model.NewIntVar(Domain(hard_start, hard_end))
|
||||
: end;
|
||||
: tasks[t].end;
|
||||
const IntervalVar local_interval = cp_model.NewOptionalIntervalVar(
|
||||
local_start, local_duration, local_end, presence);
|
||||
local_start, local_duration, local_end, local_presence);
|
||||
|
||||
// Link local and global variables.
|
||||
if (absl::GetFlag(FLAGS_use_optional_variables)) {
|
||||
cp_model.AddEquality(start, local_start).OnlyEnforceIf(presence);
|
||||
cp_model.AddEquality(end, local_end).OnlyEnforceIf(presence);
|
||||
cp_model.AddEquality(tasks[t].start, local_start)
|
||||
.OnlyEnforceIf(local_presence);
|
||||
cp_model.AddEquality(tasks[t].end, local_end)
|
||||
.OnlyEnforceIf(local_presence);
|
||||
|
||||
// TODO(user): Experiment with the following implication.
|
||||
cp_model.AddEquality(duration, local_duration)
|
||||
.OnlyEnforceIf(presence);
|
||||
cp_model.AddEquality(tasks[t].duration, task.duration(a))
|
||||
.OnlyEnforceIf(local_presence);
|
||||
}
|
||||
|
||||
// Record relevant variables for later use.
|
||||
const int m = task.machine(a);
|
||||
machine_to_intervals[m].push_back(local_interval);
|
||||
machine_to_jobs[m].push_back(j);
|
||||
machine_to_starts[m].push_back(local_start);
|
||||
machine_to_ends[m].push_back(local_end);
|
||||
machine_to_presences[m].push_back(presence);
|
||||
|
||||
// Add cost if present.
|
||||
if (task.cost_size() > 0) {
|
||||
objective_vars.push_back(presence);
|
||||
objective_coeffs.push_back(task.cost(a));
|
||||
}
|
||||
// Collect presence variables.
|
||||
presences.push_back(presence);
|
||||
alt_data.push_back(
|
||||
{local_interval, local_start, local_end, local_presence});
|
||||
}
|
||||
// Exactly one alternative interval is present.
|
||||
cp_model.AddEquality(LinearExpr::BooleanSum(presences), 1);
|
||||
std::vector<BoolVar> interval_presences;
|
||||
for (const AlternativeTaskData& alternative : alt_data) {
|
||||
interval_presences.push_back(alternative.presence);
|
||||
}
|
||||
cp_model.AddEquality(LinearExpr::BooleanSum(interval_presences), 1);
|
||||
|
||||
// Implement supporting literals for the duration of the main interval.
|
||||
if (duration_supports.size() > 1) { // duration is not fixed.
|
||||
for (const auto& duration_alternative_indices : duration_supports) {
|
||||
const int64 value = duration_alternative_indices.first;
|
||||
const BoolVar duration_eq_value = cp_model.NewBoolVar();
|
||||
|
||||
// duration_eq_value <=> duration == value.
|
||||
cp_model.AddEquality(tasks[t].duration, value)
|
||||
.OnlyEnforceIf(duration_eq_value);
|
||||
cp_model.AddNotEqual(tasks[t].duration, value)
|
||||
.OnlyEnforceIf(duration_eq_value.Not());
|
||||
// Implement the support part. If all literals pointing to the same
|
||||
// duration are false, then the duration cannot take this value.
|
||||
std::vector<BoolVar> support_clause;
|
||||
for (const int a : duration_alternative_indices.second) {
|
||||
support_clause.push_back(alt_data[a].presence);
|
||||
}
|
||||
support_clause.push_back(duration_eq_value.Not());
|
||||
cp_model.AddBoolOr(support_clause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chain the alternative tasks belonging to the same job.
|
||||
if (t > 0 && absl::GetFlag(FLAGS_use_expanded_precedences)) {
|
||||
const std::vector<AlternativeTaskData>& prev_data =
|
||||
job_task_to_alternatives[j][t - 1];
|
||||
const std::vector<AlternativeTaskData>& curr_data =
|
||||
job_task_to_alternatives[j][t];
|
||||
for (int p = 0; p < prev_data.size(); ++p) {
|
||||
const IntVar previous_end = prev_data[p].end;
|
||||
const BoolVar previous_presence = prev_data[p].presence;
|
||||
for (int c = 0; c < curr_data.size(); ++c) {
|
||||
const IntVar current_start = curr_data[c].start;
|
||||
const BoolVar current_presence = curr_data[c].presence;
|
||||
cp_model.AddLessOrEqual(previous_end, current_start)
|
||||
.OnlyEnforceIf({previous_presence, current_presence});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The makespan will be greater than the end of each job.
|
||||
if (problem.makespan_cost_per_time_unit() != 0L) {
|
||||
cp_model.AddLessOrEqual(previous_end, makespan);
|
||||
}
|
||||
// Tasks or alternative tasks are added to machines one by one.
|
||||
// This structure records the characteristics of each task added on a machine.
|
||||
// This information is indexed on each vector by the order of addition.
|
||||
struct MachineTaskData {
|
||||
IntervalVar interval;
|
||||
int job;
|
||||
IntVar start;
|
||||
int64 duration;
|
||||
IntVar end;
|
||||
BoolVar presence;
|
||||
};
|
||||
void CreateMachines(
|
||||
const JsspInputProblem& problem,
|
||||
const std::vector<std::vector<std::vector<AlternativeTaskData>>>&
|
||||
job_task_to_alternatives,
|
||||
IntervalVar makespan_interval, CpModelBuilder& cp_model) {
|
||||
const int num_jobs = problem.jobs_size();
|
||||
const int num_machines = problem.machines_size();
|
||||
std::vector<std::vector<MachineTaskData>> machine_to_tasks(num_machines);
|
||||
|
||||
const int64 lateness_penalty = job.lateness_cost_per_time_unit();
|
||||
// Lateness cost.
|
||||
if (lateness_penalty != 0L) {
|
||||
const int64 due_date = job.late_due_date();
|
||||
if (due_date == 0) {
|
||||
objective_vars.push_back(previous_end);
|
||||
objective_coeffs.push_back(lateness_penalty);
|
||||
} else {
|
||||
const IntVar shifted_var =
|
||||
cp_model.NewIntVar(Domain(-due_date, horizon - due_date));
|
||||
cp_model.AddEquality(shifted_var,
|
||||
LinearExpr(previous_end).AddConstant(-due_date));
|
||||
const IntVar lateness_var = cp_model.NewIntVar(all_horizon);
|
||||
cp_model.AddMaxEquality(lateness_var,
|
||||
{cp_model.NewConstant(0), shifted_var});
|
||||
objective_vars.push_back(lateness_var);
|
||||
objective_coeffs.push_back(lateness_penalty);
|
||||
}
|
||||
}
|
||||
const int64 earliness_penalty = job.earliness_cost_per_time_unit();
|
||||
// Earliness cost.
|
||||
if (earliness_penalty != 0L) {
|
||||
const int64 due_date = job.early_due_date();
|
||||
if (due_date > 0) {
|
||||
const IntVar shifted_var =
|
||||
cp_model.NewIntVar(Domain(due_date - horizon, due_date));
|
||||
cp_model.AddEquality(LinearExpr::Sum({shifted_var, previous_end}),
|
||||
due_date);
|
||||
const IntVar earliness_var = cp_model.NewIntVar(all_horizon);
|
||||
cp_model.AddMaxEquality(earliness_var,
|
||||
{cp_model.NewConstant(0), shifted_var});
|
||||
objective_vars.push_back(earliness_var);
|
||||
objective_coeffs.push_back(earliness_penalty);
|
||||
// Fills in the machine data vector.
|
||||
for (int j = 0; j < num_jobs; ++j) {
|
||||
const Job& job = problem.jobs(j);
|
||||
const int num_tasks_in_job = job.tasks_size();
|
||||
|
||||
for (int t = 0; t < num_tasks_in_job; ++t) {
|
||||
const Task& task = job.tasks(t);
|
||||
const int num_alternatives = task.machine_size();
|
||||
CHECK_EQ(num_alternatives, task.duration_size());
|
||||
const std::vector<AlternativeTaskData>& alt_data =
|
||||
job_task_to_alternatives[j][t];
|
||||
|
||||
for (int a = 0; a < num_alternatives; ++a) {
|
||||
// Record relevant variables for later use.
|
||||
machine_to_tasks[task.machine(a)].push_back(
|
||||
{alt_data[a].interval, j, alt_data[a].start, task.duration(a),
|
||||
alt_data[a].end, alt_data[a].presence});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add one no_overlap constraint per machine.
|
||||
for (int m = 0; m < num_machines; ++m) {
|
||||
cp_model.AddNoOverlap(machine_to_intervals[m]);
|
||||
std::vector<IntervalVar> intervals;
|
||||
for (const MachineTaskData& task : machine_to_tasks[m]) {
|
||||
intervals.push_back(task.interval);
|
||||
}
|
||||
if (absl::GetFlag(FLAGS_use_interval_makespan) &&
|
||||
problem.makespan_cost_per_time_unit() != 0L) {
|
||||
intervals.push_back(makespan_interval);
|
||||
}
|
||||
cp_model.AddNoOverlap(intervals);
|
||||
}
|
||||
|
||||
// Add transition times if needed.
|
||||
for (int m = 0; m < num_machines; ++m) {
|
||||
if (problem.machines(m).has_transition_time_matrix()) {
|
||||
const int num_intervals = machine_to_tasks[m].size();
|
||||
const TransitionTimeMatrix& transitions =
|
||||
problem.machines(m).transition_time_matrix();
|
||||
const int num_intervals = machine_to_intervals[m].size();
|
||||
|
||||
// Create circuit constraint on a machine.
|
||||
// Node 0 and num_intervals + 1 are source and sink.
|
||||
CircuitConstraint circuit = cp_model.AddCircuitConstraint();
|
||||
for (int i = 0; i < num_intervals; ++i) {
|
||||
const int job_i = machine_to_jobs[m][i];
|
||||
const int job_i = machine_to_tasks[m][i].job;
|
||||
// Source to nodes.
|
||||
circuit.AddArc(0, i + 1, cp_model.NewBoolVar());
|
||||
// Node to sink.
|
||||
@@ -282,14 +366,14 @@ void Solve(const JsspInputProblem& problem) {
|
||||
// Node to node.
|
||||
for (int j = 0; j < num_intervals; ++j) {
|
||||
if (i == j) {
|
||||
circuit.AddArc(i + 1, i + 1, Not(machine_to_presences[m][i]));
|
||||
circuit.AddArc(i + 1, i + 1, Not(machine_to_tasks[m][i].presence));
|
||||
} else {
|
||||
const int job_j = machine_to_jobs[m][j];
|
||||
const int job_j = machine_to_tasks[m][i].job;
|
||||
const int64 transition =
|
||||
transitions.transition_time(job_i * num_jobs + job_j);
|
||||
const BoolVar lit = cp_model.NewBoolVar();
|
||||
const IntVar start = machine_to_starts[m][j];
|
||||
const IntVar end = machine_to_ends[m][i];
|
||||
const IntVar start = machine_to_tasks[m][j].start;
|
||||
const IntVar end = machine_to_tasks[m][i].end;
|
||||
circuit.AddArc(i + 1, j + 1, lit);
|
||||
// Push the new start with an extra transition.
|
||||
cp_model
|
||||
@@ -300,46 +384,326 @@ void Solve(const JsspInputProblem& problem) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add job precedences.
|
||||
for (const JobPrecedence& precedence : problem.precedences()) {
|
||||
const IntVar start = job_starts[precedence.second_job_index()];
|
||||
const IntVar end = job_ends[precedence.first_job_index()];
|
||||
cp_model.AddLessOrEqual(LinearExpr(end).AddConstant(precedence.min_delay()),
|
||||
start);
|
||||
// Collect all objective terms and add them to the model.
|
||||
void CreateObjective(
|
||||
const JsspInputProblem& problem,
|
||||
const std::vector<std::vector<JobTaskData>>& job_to_tasks,
|
||||
const std::vector<std::vector<std::vector<AlternativeTaskData>>>&
|
||||
job_task_to_alternatives,
|
||||
int64 horizon, IntVar makespan, CpModelBuilder& cp_model) {
|
||||
int64 objective_offset = 0;
|
||||
std::vector<IntVar> objective_vars;
|
||||
std::vector<int64> objective_coeffs;
|
||||
|
||||
const int num_jobs = problem.jobs_size();
|
||||
for (int j = 0; j < num_jobs; ++j) {
|
||||
const Job& job = problem.jobs(j);
|
||||
const int num_tasks_in_job = job.tasks_size();
|
||||
|
||||
// Add the cost associated to each task.
|
||||
for (int t = 0; t < num_tasks_in_job; ++t) {
|
||||
const Task& task = job.tasks(t);
|
||||
const int num_alternatives = task.machine_size();
|
||||
|
||||
for (int a = 0; a < num_alternatives; ++a) {
|
||||
// Add cost if present.
|
||||
if (task.cost_size() > 0) {
|
||||
objective_vars.push_back(job_task_to_alternatives[j][t][a].presence);
|
||||
objective_coeffs.push_back(task.cost(a));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Job lateness cost.
|
||||
const int64 lateness_penalty = job.lateness_cost_per_time_unit();
|
||||
if (lateness_penalty != 0L) {
|
||||
const int64 due_date = job.late_due_date();
|
||||
const IntVar job_end = job_to_tasks[j].back().end;
|
||||
if (due_date == 0) {
|
||||
objective_vars.push_back(job_end);
|
||||
objective_coeffs.push_back(lateness_penalty);
|
||||
} else {
|
||||
const IntVar lateness_var = cp_model.NewIntVar(Domain(0, horizon));
|
||||
cp_model.AddLinMaxEquality(
|
||||
lateness_var,
|
||||
{LinearExpr(0), LinearExpr(job_end).AddConstant(-due_date)});
|
||||
objective_vars.push_back(lateness_var);
|
||||
objective_coeffs.push_back(lateness_penalty);
|
||||
}
|
||||
}
|
||||
|
||||
// Job earliness cost.
|
||||
const int64 earliness_penalty = job.earliness_cost_per_time_unit();
|
||||
if (earliness_penalty != 0L) {
|
||||
const int64 due_date = job.early_due_date();
|
||||
const IntVar job_end = job_to_tasks[j].back().end;
|
||||
|
||||
if (due_date > 0) {
|
||||
const IntVar earliness_var = cp_model.NewIntVar(Domain(0, horizon));
|
||||
cp_model.AddLinMaxEquality(
|
||||
earliness_var,
|
||||
{LinearExpr(0),
|
||||
LinearExpr::Term(job_end, -1).AddConstant(due_date)});
|
||||
objective_vars.push_back(earliness_var);
|
||||
objective_coeffs.push_back(earliness_penalty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add objective.
|
||||
// Makespan objective.
|
||||
if (problem.makespan_cost_per_time_unit() != 0L) {
|
||||
objective_coeffs.push_back(problem.makespan_cost_per_time_unit());
|
||||
objective_vars.push_back(makespan);
|
||||
}
|
||||
|
||||
// Add the objective to the model.
|
||||
cp_model.Minimize(LinearExpr::ScalProd(objective_vars, objective_coeffs)
|
||||
.AddConstant(objective_offset));
|
||||
if (problem.has_scaling_factor()) {
|
||||
cp_model.ScaleObjectiveBy(problem.scaling_factor().value());
|
||||
}
|
||||
}
|
||||
|
||||
// This is a relaxation of the problem where we only consider the main tasks,
|
||||
// and not the alternate copies.
|
||||
void AddCumulativeRelaxation(
|
||||
const JsspInputProblem& problem,
|
||||
const std::vector<std::vector<JobTaskData>>& job_to_tasks,
|
||||
IntervalVar makespan_interval, CpModelBuilder& cp_model) {
|
||||
const int num_jobs = problem.jobs_size();
|
||||
const int num_machines = problem.machines_size();
|
||||
|
||||
// Build a graph where two machines are connected if they appear in the same
|
||||
// set of alternate machines for a given task.
|
||||
std::vector<absl::flat_hash_set<int>> neighbors(num_machines);
|
||||
for (int j = 0; j < num_jobs; ++j) {
|
||||
const Job& job = problem.jobs(j);
|
||||
const int num_tasks_in_job = job.tasks_size();
|
||||
for (int t = 0; t < num_tasks_in_job; ++t) {
|
||||
const Task& task = job.tasks(t);
|
||||
for (int a = 1; a < task.machine_size(); ++a) {
|
||||
neighbors[task.machine(0)].insert(task.machine(a));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search for connected components in the above graph.
|
||||
std::vector<int> components =
|
||||
util::GetConnectedComponents(num_machines, neighbors);
|
||||
absl::flat_hash_map<int, std::vector<int>> machines_per_component;
|
||||
for (int c = 0; c < components.size(); ++c) {
|
||||
machines_per_component[components[c]].push_back(c);
|
||||
}
|
||||
|
||||
const IntVar one = cp_model.NewConstant(1);
|
||||
for (const auto& it : machines_per_component) {
|
||||
// Ignore the trivial cases.
|
||||
if (it.second.size() < 2 || it.second.size() == num_machines) continue;
|
||||
|
||||
LOG(INFO) << "Found machine connected component: ["
|
||||
<< absl::StrJoin(it.second, ", ") << "]";
|
||||
absl::flat_hash_set<int> component(it.second.begin(), it.second.end());
|
||||
const IntVar capacity = cp_model.NewConstant(component.size());
|
||||
int num_intervals_in_cumulative = 0;
|
||||
CumulativeConstraint cumul = cp_model.AddCumulative(capacity);
|
||||
for (int j = 0; j < num_jobs; ++j) {
|
||||
const Job& job = problem.jobs(j);
|
||||
const int num_tasks_in_job = job.tasks_size();
|
||||
for (int t = 0; t < num_tasks_in_job; ++t) {
|
||||
const Task& task = job.tasks(t);
|
||||
for (const int m : task.machine()) {
|
||||
if (component.contains(m)) {
|
||||
cumul.AddDemand(job_to_tasks[j][t].interval, one);
|
||||
num_intervals_in_cumulative++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (absl::GetFlag(FLAGS_use_interval_makespan)) {
|
||||
cumul.AddDemand(makespan_interval, capacity);
|
||||
}
|
||||
LOG(INFO) << " - created cumulative with " << num_intervals_in_cumulative
|
||||
<< " intervals";
|
||||
}
|
||||
}
|
||||
|
||||
// There are two linear redundant constraints.
|
||||
//
|
||||
// The first one states that the sum of durations of all tasks is a lower bound
|
||||
// of the makespan * number of machines.
|
||||
//
|
||||
// The second one takes a suffix of one job chain, and states that the start of
|
||||
// the suffix + the sum of all task durations in the suffix is a lower bound of
|
||||
// the makespan.
|
||||
void AddMakespanRedundantConstraints(
|
||||
const JsspInputProblem& problem,
|
||||
const std::vector<std::vector<JobTaskData>>& job_to_tasks, IntVar makespan,
|
||||
bool has_variable_duration_tasks, CpModelBuilder& cp_model) {
|
||||
const int num_jobs = problem.jobs_size();
|
||||
const int num_machines = problem.machines_size();
|
||||
|
||||
// Global energetic reasoning.
|
||||
std::vector<IntVar> all_task_durations;
|
||||
for (const std::vector<JobTaskData>& tasks : job_to_tasks) {
|
||||
for (const JobTaskData& task : tasks) {
|
||||
all_task_durations.push_back(task.duration);
|
||||
}
|
||||
}
|
||||
cp_model.AddLessOrEqual(LinearExpr::Sum(all_task_durations),
|
||||
LinearExpr::Term(makespan, num_machines));
|
||||
|
||||
// Suffix linear equations.
|
||||
if (has_variable_duration_tasks) {
|
||||
for (int j = 0; j < num_jobs; ++j) {
|
||||
const int job_length = job_to_tasks[j].size();
|
||||
const int start_suffix = std::max(
|
||||
0, job_length - absl::GetFlag(FLAGS_job_suffix_relaxation_length));
|
||||
for (int first_t = start_suffix; first_t + 1 < job_length; ++first_t) {
|
||||
std::vector<IntVar> terms = {job_to_tasks[j][first_t].start};
|
||||
for (int t = first_t; t < job_length; ++t) {
|
||||
terms.push_back(job_to_tasks[j][t].duration);
|
||||
}
|
||||
cp_model.AddLessOrEqual(LinearExpr::Sum(terms), makespan);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Solve a JobShop scheduling problem using CP-SAT.
|
||||
void Solve(const JsspInputProblem& problem) {
|
||||
if (absl::GetFlag(FLAGS_display_model)) {
|
||||
LOG(INFO) << problem.DebugString();
|
||||
}
|
||||
|
||||
CpModelBuilder cp_model;
|
||||
|
||||
// Compute an over estimate of the horizon.
|
||||
const int64 horizon = absl::GetFlag(FLAGS_horizon) != -1
|
||||
? absl::GetFlag(FLAGS_horizon)
|
||||
: ComputeHorizon(problem);
|
||||
|
||||
// Create the main job structure.
|
||||
const int num_jobs = problem.jobs_size();
|
||||
std::vector<std::vector<JobTaskData>> job_to_tasks(num_jobs);
|
||||
bool has_variable_duration_tasks = false;
|
||||
CreateJobs(problem, horizon, job_to_tasks, has_variable_duration_tasks,
|
||||
cp_model);
|
||||
|
||||
// For each task of each jobs, create the alternative copies if needed and
|
||||
// fill in the AlternativeTaskData vector.
|
||||
std::vector<std::vector<std::vector<AlternativeTaskData>>>
|
||||
job_task_to_alternatives(num_jobs);
|
||||
CreateAlternativeTasks(problem, job_to_tasks, horizon,
|
||||
job_task_to_alternatives, cp_model);
|
||||
|
||||
// Create the makespan variable and interval.
|
||||
// If this flag is true, we will add to each no overlap constraint a special
|
||||
// "makespan interval" that must necessarily be last by construction. This
|
||||
// gives us a better lower bound on the makespan because this way we known
|
||||
// that it must be after all other intervals in each no-overlap constraint.
|
||||
//
|
||||
// Otherwise, we will just add precence constraints between the last task of
|
||||
// each job and the makespan variable. Alternatively, we could have added a
|
||||
// precedence relation between all tasks and the makespan for a similar
|
||||
// propagation thanks to our "precedence" propagator in the dijsunctive but
|
||||
// that was slower than the interval trick when I tried.
|
||||
const IntVar makespan = cp_model.NewIntVar(Domain(0, horizon));
|
||||
IntervalVar makespan_interval;
|
||||
if (absl::GetFlag(FLAGS_use_interval_makespan)) {
|
||||
makespan_interval = cp_model.NewIntervalVar(
|
||||
/*start=*/makespan,
|
||||
/*size=*/cp_model.NewIntVar(Domain(1, horizon)),
|
||||
/*end=*/cp_model.NewIntVar(Domain(horizon + 1)));
|
||||
} else if (problem.makespan_cost_per_time_unit() != 0L) {
|
||||
for (int j = 0; j < num_jobs; ++j) {
|
||||
// The makespan will be greater than the end of each job.
|
||||
// This is not needed if we add the makespan "interval" to each
|
||||
// disjunctive.
|
||||
cp_model.AddLessOrEqual(job_to_tasks[j].back().end, makespan);
|
||||
}
|
||||
}
|
||||
|
||||
// Machine constraints.
|
||||
CreateMachines(problem, job_task_to_alternatives, makespan_interval,
|
||||
cp_model);
|
||||
|
||||
// Try to detect connected components of alternative machines.
|
||||
// If this is happens, we can add a cumulative constraint as a relaxation of
|
||||
// all no_ovelap constraints on the set of alternative machines.
|
||||
if (absl::GetFlag(FLAGS_use_cumulative_relaxation)) {
|
||||
AddCumulativeRelaxation(problem, job_to_tasks, makespan_interval, cp_model);
|
||||
}
|
||||
|
||||
// Various redundant constraints. They are mostly here to improve the LP
|
||||
// relaxation.
|
||||
if (problem.makespan_cost_per_time_unit() != 0L) {
|
||||
AddMakespanRedundantConstraints(problem, job_to_tasks, makespan,
|
||||
has_variable_duration_tasks, cp_model);
|
||||
}
|
||||
|
||||
// Add job precedences.
|
||||
for (const JobPrecedence& precedence : problem.precedences()) {
|
||||
const IntVar start =
|
||||
job_to_tasks[precedence.second_job_index()].front().start;
|
||||
const IntVar end = job_to_tasks[precedence.first_job_index()].back().end;
|
||||
cp_model.AddLessOrEqual(LinearExpr(end).AddConstant(precedence.min_delay()),
|
||||
start);
|
||||
}
|
||||
|
||||
// Objective.
|
||||
CreateObjective(problem, job_to_tasks, job_task_to_alternatives, horizon,
|
||||
makespan, cp_model);
|
||||
|
||||
// Decision strategy.
|
||||
cp_model.AddDecisionStrategy(task_starts,
|
||||
std::vector<IntVar> all_task_starts;
|
||||
for (const std::vector<JobTaskData>& job : job_to_tasks) {
|
||||
for (const JobTaskData& task : job) {
|
||||
all_task_starts.push_back(task.start);
|
||||
}
|
||||
}
|
||||
cp_model.AddDecisionStrategy(all_task_starts,
|
||||
DecisionStrategyProto::CHOOSE_LOWEST_MIN,
|
||||
DecisionStrategyProto::SELECT_MIN_VALUE);
|
||||
|
||||
LOG(INFO) << "#machines:" << num_machines;
|
||||
// Display problem statistics.
|
||||
int num_tasks = 0;
|
||||
int num_tasks_with_variable_duration = 0;
|
||||
int num_tasks_with_alternatives = 0;
|
||||
for (const std::vector<JobTaskData>& job : job_to_tasks) {
|
||||
num_tasks += job.size();
|
||||
for (const JobTaskData& task : job) {
|
||||
if (task.duration.Proto().domain_size() != 2 ||
|
||||
task.duration.Proto().domain(0) != task.duration.Proto().domain(1)) {
|
||||
num_tasks_with_variable_duration++;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const std::vector<std::vector<AlternativeTaskData>>&
|
||||
task_to_alternatives : job_task_to_alternatives) {
|
||||
for (const std::vector<AlternativeTaskData>& alternatives :
|
||||
task_to_alternatives) {
|
||||
if (alternatives.size() > 1) num_tasks_with_alternatives++;
|
||||
}
|
||||
}
|
||||
|
||||
LOG(INFO) << "#machines:" << problem.machines_size();
|
||||
LOG(INFO) << "#jobs:" << num_jobs;
|
||||
LOG(INFO) << "horizon:" << horizon;
|
||||
LOG(INFO) << "#tasks: " << num_tasks;
|
||||
LOG(INFO) << "#tasks with alternative: " << num_tasks_with_alternatives;
|
||||
LOG(INFO) << "#tasks with variable duration: "
|
||||
<< num_tasks_with_variable_duration;
|
||||
|
||||
if (absl::GetFlag(FLAGS_display_sat_model)) {
|
||||
LOG(INFO) << cp_model.Proto().DebugString();
|
||||
}
|
||||
|
||||
LOG(INFO) << CpModelStats(cp_model.Proto());
|
||||
|
||||
Model model;
|
||||
model.Add(NewSatParameters(absl::GetFlag(FLAGS_params)));
|
||||
|
||||
const CpSolverResponse response = SolveCpModel(cp_model.Build(), &model);
|
||||
LOG(INFO) << CpSolverResponseStats(response);
|
||||
|
||||
// Abort if we don't have any solution.
|
||||
if (response.status() != CpSolverStatus::OPTIMAL &&
|
||||
@@ -350,18 +714,20 @@ void Solve(const JsspInputProblem& problem) {
|
||||
int64 final_cost = 0;
|
||||
if (problem.makespan_cost_per_time_unit() != 0) {
|
||||
int64 makespan = 0;
|
||||
for (IntVar v : job_ends) {
|
||||
makespan = std::max(makespan, SolutionIntegerValue(response, v));
|
||||
for (const std::vector<JobTaskData>& tasks : job_to_tasks) {
|
||||
const IntVar job_end = tasks.back().end;
|
||||
makespan = std::max(makespan, SolutionIntegerValue(response, job_end));
|
||||
}
|
||||
final_cost += makespan * problem.makespan_cost_per_time_unit();
|
||||
}
|
||||
|
||||
for (int i = 0; i < job_ends.size(); ++i) {
|
||||
const int64 early_due_date = problem.jobs(i).early_due_date();
|
||||
const int64 late_due_date = problem.jobs(i).late_due_date();
|
||||
const int64 early_penalty = problem.jobs(i).earliness_cost_per_time_unit();
|
||||
const int64 late_penalty = problem.jobs(i).lateness_cost_per_time_unit();
|
||||
const int64 end = SolutionIntegerValue(response, job_ends[i]);
|
||||
for (int j = 0; j < num_jobs; ++j) {
|
||||
const int64 early_due_date = problem.jobs(j).early_due_date();
|
||||
const int64 late_due_date = problem.jobs(j).late_due_date();
|
||||
const int64 early_penalty = problem.jobs(j).earliness_cost_per_time_unit();
|
||||
const int64 late_penalty = problem.jobs(j).lateness_cost_per_time_unit();
|
||||
const int64 end =
|
||||
SolutionIntegerValue(response, job_to_tasks[j].back().end);
|
||||
if (end < early_due_date && early_penalty != 0) {
|
||||
final_cost += (early_due_date - end) * early_penalty;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "absl/flags/flag.h"
|
||||
#include "ortools/base/commandlineflags.h"
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/sat/cp_model.h"
|
||||
@@ -49,7 +50,7 @@ void MultiKnapsackSat(int scaling, const std::string& params) {
|
||||
const int num_items = scaling * kNumItems;
|
||||
const int num_bins = scaling;
|
||||
|
||||
std::vector<std::vector<BoolVar> > items_in_bins(num_bins);
|
||||
std::vector<std::vector<BoolVar>> items_in_bins(num_bins);
|
||||
for (int b = 0; b < num_bins; ++b) {
|
||||
for (int i = 0; i < num_items; ++i) {
|
||||
items_in_bins[b].push_back(builder.NewBoolVar());
|
||||
|
||||
@@ -186,7 +186,7 @@ class SatCnfReader {
|
||||
// Since the problem name is not stored in the cnf format, we infer it from
|
||||
// the file name.
|
||||
static std::string ExtractProblemName(const std::string& filename) {
|
||||
const int found = filename.find_last_of("/");
|
||||
const int found = filename.find_last_of('/');
|
||||
const std::string problem_name =
|
||||
found != std::string::npos ? filename.substr(found + 1) : filename;
|
||||
return problem_name;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/flags/flag.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/match.h"
|
||||
@@ -239,6 +240,7 @@ int Run() {
|
||||
|
||||
// The SAT competition requires a particular exit code and since we don't
|
||||
// really use it for any other purpose, we comply.
|
||||
if (response.status() == CpSolverStatus::OPTIMAL) return 10;
|
||||
if (response.status() == CpSolverStatus::FEASIBLE) return 10;
|
||||
if (response.status() == CpSolverStatus::INFEASIBLE) return 20;
|
||||
return EXIT_SUCCESS;
|
||||
@@ -300,7 +302,7 @@ int Run() {
|
||||
CHECK(!absl::GetFlag(FLAGS_reduce_memory_usage)) << "incompatible";
|
||||
CHECK(!absl::GetFlag(FLAGS_presolve)) << "incompatible";
|
||||
LOG(INFO) << "Finding symmetries of the problem.";
|
||||
std::vector<std::unique_ptr<SparsePermutation> > generators;
|
||||
std::vector<std::unique_ptr<SparsePermutation>> generators;
|
||||
FindLinearBooleanProblemSymmetries(problem, &generators);
|
||||
std::unique_ptr<SymmetryPropagator> propagator(new SymmetryPropagator);
|
||||
for (int i = 0; i < generators.size(); ++i) {
|
||||
@@ -366,7 +368,7 @@ int Run() {
|
||||
if (result == SatSolver::FEASIBLE) {
|
||||
if (absl::GetFlag(FLAGS_fu_malik) || absl::GetFlag(FLAGS_linear_scan) ||
|
||||
absl::GetFlag(FLAGS_wpm1) || absl::GetFlag(FLAGS_core_enc)) {
|
||||
printf("s OPTIMUM FOUND\n");
|
||||
absl::PrintF("s OPTIMUM FOUND\n");
|
||||
CHECK(!solution.empty());
|
||||
const Coefficient objective = ComputeObjectiveValue(problem, solution);
|
||||
scaled_best_bound = AddOffsetAndScaleObjectiveValue(problem, objective);
|
||||
@@ -377,13 +379,13 @@ int Run() {
|
||||
problem = original_problem;
|
||||
}
|
||||
} else {
|
||||
printf("s SATISFIABLE\n");
|
||||
absl::PrintF("s SATISFIABLE\n");
|
||||
}
|
||||
|
||||
// Check and output the solution.
|
||||
CHECK(IsAssignmentValid(problem, solution));
|
||||
if (absl::GetFlag(FLAGS_output_cnf_solution)) {
|
||||
printf("v %s\n", SolutionString(problem, solution).c_str());
|
||||
absl::PrintF("v %s\n", SolutionString(problem, solution));
|
||||
}
|
||||
if (!absl::GetFlag(FLAGS_output).empty()) {
|
||||
CHECK(!absl::GetFlag(FLAGS_reduce_memory_usage)) << "incompatible";
|
||||
@@ -400,36 +402,32 @@ int Run() {
|
||||
}
|
||||
}
|
||||
if (result == SatSolver::INFEASIBLE) {
|
||||
printf("s UNSATISFIABLE\n");
|
||||
absl::PrintF("s UNSATISFIABLE\n");
|
||||
}
|
||||
|
||||
// Print status.
|
||||
printf("c status: %s\n", SatStatusString(result).c_str());
|
||||
absl::PrintF("c status: %s\n", SatStatusString(result));
|
||||
|
||||
// Print objective value.
|
||||
if (solution.empty()) {
|
||||
printf("c objective: na\n");
|
||||
printf("c best bound: na\n");
|
||||
absl::PrintF("c objective: na\n");
|
||||
absl::PrintF("c best bound: na\n");
|
||||
} else {
|
||||
const Coefficient objective = ComputeObjectiveValue(problem, solution);
|
||||
printf("c objective: %.16g\n",
|
||||
AddOffsetAndScaleObjectiveValue(problem, objective));
|
||||
printf("c best bound: %.16g\n", scaled_best_bound);
|
||||
absl::PrintF("c objective: %.16g\n",
|
||||
AddOffsetAndScaleObjectiveValue(problem, objective));
|
||||
absl::PrintF("c best bound: %.16g\n", scaled_best_bound);
|
||||
}
|
||||
|
||||
// Print final statistics.
|
||||
printf("c booleans: %d\n", solver->NumVariables());
|
||||
absl::PrintF("c booleans: %d\n", solver->NumVariables());
|
||||
absl::PrintF("c conflicts: %d\n", solver->num_failures());
|
||||
absl::PrintF("c branches: %d\n", solver->num_branches());
|
||||
absl::PrintF("c propagations: %d\n", solver->num_propagations());
|
||||
printf("c walltime: %f\n", wall_timer.Get());
|
||||
printf("c usertime: %f\n", user_timer.Get());
|
||||
printf("c deterministic_time: %f\n", solver->deterministic_time());
|
||||
absl::PrintF("c walltime: %f\n", wall_timer.Get());
|
||||
absl::PrintF("c usertime: %f\n", user_timer.Get());
|
||||
absl::PrintF("c deterministic_time: %f\n", solver->deterministic_time());
|
||||
|
||||
// The SAT competition requires a particular exit code and since we don't
|
||||
// really use it for any other purpose, we comply.
|
||||
if (result == SatSolver::FEASIBLE) return 10;
|
||||
if (result == SatSolver::INFEASIBLE) return 20;
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/flags/flag.h"
|
||||
#include "absl/strings/numbers.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "ortools/base/commandlineflags.h"
|
||||
@@ -63,10 +64,10 @@ class ShiftMinimizationParser {
|
||||
num_workers_read_(0) {}
|
||||
|
||||
const std::vector<Job>& jobs() const { return jobs_; }
|
||||
const std::vector<std::vector<int> >& possible_jobs_per_worker() const {
|
||||
const std::vector<std::vector<int>>& possible_jobs_per_worker() const {
|
||||
return possible_jobs_per_worker_;
|
||||
}
|
||||
const std::vector<std::vector<Assignment> >& possible_assignments_per_job()
|
||||
const std::vector<std::vector<Assignment>>& possible_assignments_per_job()
|
||||
const {
|
||||
return possible_assignments_per_job_;
|
||||
}
|
||||
@@ -160,8 +161,8 @@ class ShiftMinimizationParser {
|
||||
}
|
||||
|
||||
std::vector<Job> jobs_;
|
||||
std::vector<std::vector<int> > possible_jobs_per_worker_;
|
||||
std::vector<std::vector<Assignment> > possible_assignments_per_job_;
|
||||
std::vector<std::vector<int>> possible_jobs_per_worker_;
|
||||
std::vector<std::vector<Assignment>> possible_assignments_per_job_;
|
||||
LoadStatus load_status_;
|
||||
int declared_num_jobs_;
|
||||
int declared_num_workers_;
|
||||
@@ -186,8 +187,8 @@ void LoadAndSolve(const std::string& file_name) {
|
||||
const int num_jobs = jobs.size();
|
||||
|
||||
std::vector<BoolVar> active_workers(num_workers);
|
||||
std::vector<std::vector<BoolVar> > worker_job_vars(num_workers);
|
||||
std::vector<std::vector<BoolVar> > possible_workers_per_job(num_jobs);
|
||||
std::vector<std::vector<BoolVar>> worker_job_vars(num_workers);
|
||||
std::vector<std::vector<BoolVar>> possible_workers_per_job(num_jobs);
|
||||
|
||||
for (int w = 0; w < num_workers; ++w) {
|
||||
// Status variables for workers, are they active or not?
|
||||
@@ -235,7 +236,7 @@ void LoadAndSolve(const std::string& file_name) {
|
||||
// then the number of active workers on these jobs is equal to the number of
|
||||
// active jobs.
|
||||
std::set<int> time_points;
|
||||
std::set<std::vector<int> > visited_job_lists;
|
||||
std::set<std::vector<int>> visited_job_lists;
|
||||
|
||||
for (int j = 0; j < num_jobs; ++j) {
|
||||
time_points.insert(parser.jobs()[j].start);
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
// - If team A meets team B, the reverse match cannot happen less that 6 weeks
|
||||
// after.
|
||||
//
|
||||
// We model this problem with three matrices of variables, each with
|
||||
// In the opponent model, we use three matrices of variables, each with
|
||||
// num_teams rows and 2*(num_teams - 1) columns: the var at position [i][j]
|
||||
// corresponds to the match of team #i at day #j. There are
|
||||
// 2*(num_teams - 1) columns because each team meets num_teams - 1
|
||||
@@ -38,32 +38,38 @@
|
||||
// - The 'signed_opponent' var [i][j] is the 'opponent' var [i][j] +
|
||||
// num_teams * the 'home_away' var [i][j].
|
||||
//
|
||||
// This aggregated variable will be useful to state constraints of the model
|
||||
// and to do search on it.
|
||||
// In the fixture model, we have a cube of Boolean variables fixtures.
|
||||
// fixtures[d][i][j] is true if team i plays team j at home on day d.
|
||||
// We also introduces a variable at_home[d][i] which is true if team i
|
||||
// plays any opponent at home on day d.
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "ortools/base/commandlineflags.h"
|
||||
#include "ortools/base/integral_types.h"
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/sat/cp_model.h"
|
||||
#include "ortools/sat/cp_model.pb.h"
|
||||
#include "ortools/sat/model.h"
|
||||
|
||||
// Problem main flags.
|
||||
ABSL_FLAG(int, num_teams, 10, "Number of teams in the problem.");
|
||||
ABSL_FLAG(std::string, params, "", "Sat parameters.");
|
||||
ABSL_FLAG(int, model, 1, "1 = opponent model, 2 = fixture model");
|
||||
|
||||
namespace operations_research {
|
||||
namespace sat {
|
||||
|
||||
void FirstModel(int num_teams) {
|
||||
void OpponentModel(int num_teams) {
|
||||
const int num_days = 2 * num_teams - 2;
|
||||
const int kNoRematch = 6;
|
||||
|
||||
CpModelBuilder builder;
|
||||
|
||||
// Calendar variables.
|
||||
std::vector<std::vector<IntVar> > opponents(num_teams);
|
||||
std::vector<std::vector<BoolVar> > home_aways(num_teams);
|
||||
std::vector<std::vector<IntVar> > signed_opponents(num_teams);
|
||||
std::vector<std::vector<IntVar>> opponents(num_teams);
|
||||
std::vector<std::vector<BoolVar>> home_aways(num_teams);
|
||||
std::vector<std::vector<IntVar>> signed_opponents(num_teams);
|
||||
|
||||
for (int t = 0; t < num_teams; ++t) {
|
||||
for (int d = 0; d < num_days; ++d) {
|
||||
@@ -185,7 +191,7 @@ void FirstModel(int num_teams) {
|
||||
}
|
||||
}
|
||||
|
||||
void SecondModel(int num_teams) {
|
||||
void FixtureModel(int num_teams) {
|
||||
const int num_days = 2 * num_teams - 2;
|
||||
// const int kNoRematch = 6;
|
||||
const int matches_per_day = num_teams - 1;
|
||||
@@ -193,7 +199,7 @@ void SecondModel(int num_teams) {
|
||||
CpModelBuilder builder;
|
||||
|
||||
// Does team i receive team j at home on day d?
|
||||
std::vector<std::vector<std::vector<BoolVar> > > fixtures(num_days);
|
||||
std::vector<std::vector<std::vector<BoolVar>>> fixtures(num_days);
|
||||
for (int d = 0; d < num_days; ++d) {
|
||||
fixtures[d].resize(num_teams);
|
||||
for (int i = 0; i < num_teams; ++i) {
|
||||
@@ -209,14 +215,14 @@ void SecondModel(int num_teams) {
|
||||
}
|
||||
|
||||
// Is team t at home on day d?
|
||||
std::vector<std::vector<BoolVar> > at_home(num_days);
|
||||
std::vector<std::vector<BoolVar>> at_home(num_days);
|
||||
for (int d = 0; d < num_days; ++d) {
|
||||
for (int t = 0; t < num_teams; ++t) {
|
||||
at_home[d].push_back(builder.NewBoolVar());
|
||||
}
|
||||
}
|
||||
|
||||
// Each day, Team t plays either at home or away.
|
||||
// Each day, Team t plays another team, either at home or away.
|
||||
for (int d = 0; d < num_days; ++d) {
|
||||
for (int team = 0; team < num_teams; ++team) {
|
||||
std::vector<BoolVar> possible_opponents;
|
||||
@@ -317,11 +323,15 @@ static const char kUsage[] =
|
||||
"There is no output besides the debug LOGs of the solver.";
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
absl::SetProgramUsageMessage(kUsage);
|
||||
absl::SetFlag(&FLAGS_logtostderr, true);
|
||||
absl::ParseCommandLine(argc, argv);
|
||||
CHECK_EQ(0, absl::GetFlag(FLAGS_num_teams) % 2)
|
||||
<< "The number of teams must be even";
|
||||
CHECK_GE(absl::GetFlag(FLAGS_num_teams), 2) << "At least 2 teams";
|
||||
operations_research::sat::SecondModel(absl::GetFlag(FLAGS_num_teams));
|
||||
if (absl::GetFlag(FLAGS_model) == 1) {
|
||||
operations_research::sat::OpponentModel(absl::GetFlag(FLAGS_num_teams));
|
||||
} else {
|
||||
operations_research::sat::FixtureModel(absl::GetFlag(FLAGS_num_teams));
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <numeric>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/flags/flag.h"
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/numbers.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
@@ -118,8 +119,7 @@ void Solve(const std::vector<int64>& durations,
|
||||
|
||||
// TODO(user): We can't set an objective upper bound with the current cp_model
|
||||
// interface, so we can't use heuristic or absl::GetFlag(FLAGS_upper_bound)
|
||||
// here. The best is
|
||||
// probably to provide a "solution hint" instead.
|
||||
// here. The best is probably to provide a "solution hint" instead.
|
||||
//
|
||||
// Set a known upper bound (or use the flag). This has a bigger impact than
|
||||
// can be expected at first:
|
||||
@@ -176,7 +176,7 @@ void Solve(const std::vector<int64>& durations,
|
||||
for (int i = 0; i < num_tasks; ++i) {
|
||||
const int64 end = SolutionIntegerMin(r, task_ends[i]);
|
||||
CHECK_EQ(end, SolutionIntegerMax(r, task_ends[i]));
|
||||
objective += weights[i] * std::max<int64>(0ll, end - due_dates[i]);
|
||||
objective += weights[i] * std::max<int64>(int64{0}, end - due_dates[i]);
|
||||
}
|
||||
LOG(INFO) << "Cost " << objective;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user