23#include "absl/container/flat_hash_set.h"
24#include "absl/log/log.h"
25#include "absl/status/status.h"
26#include "absl/strings/str_format.h"
27#include "absl/types/span.h"
38 solve_for_rooms_ = room_count_ > 0;
41 if (!solve_for_rooms_) {
47 if (course.consecutive_slots_count() != 1 &&
48 course.consecutive_slots_count() != 2) {
49 return absl::InvalidArgumentError(absl::StrFormat(
50 "The course titled %s has %d consecutive time slots specified when "
51 "it can only have 1 or 2.",
52 course.display_name(), course.consecutive_slots_count()));
55 if (course.teacher_section_counts_size() != course.teacher_indices_size()) {
56 return absl::InvalidArgumentError(
57 absl::StrFormat(
"The course titled %s should have the same number of "
58 "teacher indices and section numbers.",
59 course.display_name()));
62 for (
const int teacher_index : course.teacher_indices()) {
64 return absl::InvalidArgumentError(absl::StrFormat(
65 "The course titled %s has teacher %d assigned to it but there are "
67 course.display_name(), teacher_index, model.
teachers_size()));
71 for (
const int room_index : course.room_indices()) {
73 return absl::InvalidArgumentError(
74 absl::StrFormat(
"The course titled %s is slotted for room index %d "
75 "but there are only %d rooms.",
76 course.display_name(), room_index, room_count_));
83 teacher_to_restricted_slots_ =
85 for (
int teacher_index = 0; teacher_index < model.
teachers_size();
87 for (
const int restricted_slot :
89 if (restricted_slot >= time_slot_count_) {
90 return absl::InvalidArgumentError(
91 absl::StrFormat(
"Teacher with name %s has restricted time slot %d "
92 "but there are only %d time slots.",
94 restricted_slot, time_slot_count_));
96 teacher_to_restricted_slots_[teacher_index].insert(restricted_slot);
105 course_to_classes_ = std::vector<std::vector<int>>(model.
courses_size());
107 teacher_to_classes_ =
108 std::vector<absl::flat_hash_set<int>>(model.
teachers_size());
110 absl::flat_hash_set<int> singleton_courses;
111 int flattened_course_index = 0;
112 for (
int course_index = 0; course_index < model.
courses_size();
115 int total_section_count = 0;
120 flattened_course_index);
121 course_to_classes_[course_index].push_back(flattened_course_index);
122 ++flattened_course_index;
126 if (total_section_count == 1) {
127 singleton_courses.insert(course_index);
130 class_count_ = flattened_course_index;
135 for (
const int course_index : student.course_indices()) {
137 return absl::InvalidArgumentError(absl::StrFormat(
138 "Student with name %s has course index %d but there are only %d "
140 student.display_name(), course_index, model.
courses_size()));
143 InsertSortedPairs(std::vector<int>(student.course_indices().begin(),
144 student.course_indices().end()),
148 LOG(INFO) <<
"Number of days: " << model.
days_count();
150 LOG(INFO) <<
"Total number of time slots: " << time_slot_count_;
151 LOG(INFO) <<
"Number of courses: " << model.
courses_size();
152 LOG(INFO) <<
"Total number of classes: " << class_count_;
153 LOG(INFO) <<
"Number of teachers: " << model.
teachers_size();
154 LOG(INFO) <<
"Number of students: " << model.
students_size();
155 if (solve_for_rooms_) {
156 LOG(INFO) <<
"Number of rooms: " << model.
rooms_size();
159 return absl::OkStatus();
167 if (!validation_status.ok()) {
183 if (!verifier_status.ok()) {
195 result = ScheduleCourses(class_conflicts, model);
199 result.
set_message(
"The problem is infeasible with the given courses.");
204 ConflictPairs class_conflicts_to_try = AssignStudents(model, &result);
206 if (class_conflicts_to_try.empty())
return result;
208 std::vector<std::pair<int, int>> conflicts(class_conflicts_to_try.begin(),
209 class_conflicts_to_try.end());
211 const int conflicts_count = conflicts.size();
212 const int conflicts_log = conflicts_count == 1 ? 1 : log2(conflicts_count);
213 for (
int i = 0; i < conflicts_log; ++i) {
215 for (
int j = 0; j < divisions; ++j) {
216 const int start = std::floor(conflicts_count * j / divisions);
217 const int end = std::floor(conflicts_count * (j + 1) / divisions);
220 if (
end >= conflicts_count) {
221 new_class_conflicts.insert(conflicts.begin() + start, conflicts.end());
223 new_class_conflicts.insert(conflicts.begin() + start,
224 conflicts.begin() +
end);
227 result =
SolveModel(model, new_class_conflicts);
238std::vector<int> CourseSchedulingSolver::GetRoomIndices(
const Course& course) {
239 if (solve_for_rooms_) {
247void CourseSchedulingSolver::InsertSortedPairs(absl::Span<const int> list,
248 ConflictPairs* pairs) {
249 for (
int first = 1; first < list.size(); ++first) {
250 for (
int second = first; second < list.size(); ++second) {
251 pairs->insert(std::minmax(list[first - 1], list[second]));
256std::vector<absl::flat_hash_set<int>>
257CourseSchedulingSolver::GetClassesByTimeSlot(
259 std::vector<absl::flat_hash_set<int>> time_slot_to_classes(time_slot_count_);
261 for (
const ClassAssignment& class_assignment : result->class_assignments()) {
262 const int course_index = class_assignment.course_index();
263 const int section_number = class_assignment.section_number();
264 for (
const int time_slot : class_assignment.time_slots()) {
265 time_slot_to_classes[time_slot].insert(
266 course_to_classes_[course_index][section_number]);
270 return time_slot_to_classes;
273void CourseSchedulingSolver::AddVariableIfNonNull(
double coeff,
276 if (var ==
nullptr)
return;
278 ct->SetCoefficient(var, coeff);
283 LOG(INFO) <<
"Starting schedule courses solver with "
284 << class_conflicts.size() <<
" class conflicts.";
285 MPSolver mip_solver(
"CourseSchedulingMIP",
287 const double kInfinity = std::numeric_limits<double>::infinity();
293 std::vector<std::vector<std::vector<MPVariable*>>> variables(
295 std::vector<std::vector<MPVariable*>>(
296 time_slot_count_, std::vector<MPVariable*>(room_count_,
nullptr)));
297 for (
int proto_index = 0; proto_index < model.courses_size(); ++proto_index) {
298 const Course& course = model.courses(proto_index);
299 const int course_index = course_to_classes_[proto_index][0];
300 int total_section_index = 0;
301 for (
int i = 0;
i < course.teacher_section_counts_size(); ++
i) {
302 const int teacher = course.teacher_indices(i);
303 const int section_count = course.teacher_section_counts(i);
304 for (
int section = 0; section < section_count; ++section) {
305 for (
int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
306 if (teacher_to_restricted_slots_[teacher].contains(time_slot)) {
310 for (
const int room : GetRoomIndices(course)) {
311 variables[course_index + total_section_index][time_slot][room] =
312 mip_solver.MakeBoolVar(absl::StrFormat(
313 "x_%d_%d_%d", course_index + total_section_index, time_slot,
317 ++total_section_index;
322 std::vector<std::vector<MPVariable*>> intermediate_vars(
323 class_count_, std::vector<MPVariable*>(time_slot_count_));
324 for (
int class_index = 0; class_index < class_count_; ++class_index) {
325 for (
int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
326 MPConstraint*
const ct = mip_solver.MakeRowConstraint(0, 0);
327 for (
int room = 0; room < room_count_; ++room) {
328 if (variables[class_index][time_slot][room] ==
nullptr)
continue;
330 ct->SetCoefficient(variables[class_index][time_slot][room], 1);
332 if (!ct->terms().empty()) {
333 intermediate_vars[class_index][time_slot] = mip_solver.MakeBoolVar(
334 absl::StrFormat(
"intermediate_%d_%d", class_index, time_slot));
335 ct->SetCoefficient(intermediate_vars[class_index][time_slot], -1);
342 for (
int day = 0; day < model.days_count(); ++day) {
343 for (
int course = 0; course < model.courses_size(); ++course) {
344 const int consecutive_slot_count =
345 model.courses(course).consecutive_slots_count();
346 for (
const int class_index : course_to_classes_[course]) {
347 MPConstraint*
const ct =
348 mip_solver.MakeRowConstraint(0, consecutive_slot_count);
349 for (
int daily_time_slot = 0;
350 daily_time_slot < model.daily_time_slot_count();
352 AddVariableIfNonNull(
354 intermediate_vars[class_index]
355 [(day * model.daily_time_slot_count()) +
365 for (
int course = 0; course < model.courses_size(); ++course) {
366 const int meeting_count = model.courses(course).meetings_count();
367 const int consecutive_slot_count =
368 model.courses(course).consecutive_slots_count();
369 for (
const int class_index : course_to_classes_[course]) {
370 MPConstraint*
const ct =
371 mip_solver.MakeRowConstraint(meeting_count * consecutive_slot_count,
372 meeting_count * consecutive_slot_count);
373 for (
int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
374 AddVariableIfNonNull(1, intermediate_vars[class_index][time_slot], ct);
380 for (
int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
381 for (
int teacher = 0; teacher < model.teachers_size(); ++teacher) {
382 MPConstraint*
const ct = mip_solver.MakeRowConstraint(0, 1);
383 for (
const int class_index : teacher_to_classes_[teacher]) {
384 AddVariableIfNonNull(1, intermediate_vars[class_index][time_slot], ct);
390 if (solve_for_rooms_) {
391 for (
int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
392 for (
int room = 0; room < room_count_; ++room) {
393 MPConstraint*
const ct = mip_solver.MakeRowConstraint(0, 1);
394 for (
int class_index = 0; class_index < class_count_; ++class_index) {
395 AddVariableIfNonNull(1, variables[class_index][time_slot][room], ct);
403 for (
int course = 0; course < model.courses_size(); ++course) {
404 const int consecutive_slot_count =
405 model.courses(course).consecutive_slots_count();
406 if (consecutive_slot_count == 1)
continue;
407 for (
const int class_index : course_to_classes_[course]) {
408 for (
int day = 0; day < model.days_count(); ++day) {
409 for (
int room = 0; room < room_count_; ++room) {
412 for (
int daily_time_slot = 0;
413 daily_time_slot < model.daily_time_slot_count();
415 MPConstraint*
const ct = mip_solver.MakeRowConstraint(0,
kInfinity);
416 const int time_slot_offset =
417 day * model.daily_time_slot_count() + daily_time_slot;
419 if (daily_time_slot > 0) {
420 AddVariableIfNonNull(
421 1, variables[class_index][time_slot_offset - 1][room], ct);
423 AddVariableIfNonNull(
424 -0.5, variables[class_index][time_slot_offset][room], ct);
425 if (daily_time_slot < model.daily_time_slot_count() - 1) {
426 AddVariableIfNonNull(
427 0.5, variables[class_index][time_slot_offset + 1][room], ct);
437 for (
const auto& conflict_pair : course_conflicts_) {
438 const int course_1 = conflict_pair.first;
439 const int course_2 = conflict_pair.second;
440 for (
int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
441 const int bound = course_to_classes_[course_1].size() +
442 course_to_classes_[course_2].size() - 1;
443 MPConstraint*
const ct = mip_solver.MakeRowConstraint(0, bound);
444 for (
const int class_1 : course_to_classes_[course_1]) {
445 AddVariableIfNonNull(1, intermediate_vars[class_1][time_slot], ct);
447 for (
const int class_2 : course_to_classes_[course_2]) {
448 AddVariableIfNonNull(1, intermediate_vars[class_2][time_slot], ct);
455 for (
const auto& conflict_pair : class_conflicts) {
456 for (
int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
457 MPConstraint*
const ct = mip_solver.MakeRowConstraint(0, 1);
458 AddVariableIfNonNull(1, intermediate_vars[conflict_pair.first][time_slot],
460 AddVariableIfNonNull(
461 1, intermediate_vars[conflict_pair.second][time_slot], ct);
467 CourseSchedulingResult result;
468 result.set_solver_status(MipStatusToCourseSchedulingResultStatus(status));
472 "MIP solver returned UNBOUNDED: the model is solved but the solution "
476 "MIP solver returned ABNORMAL: some error occurred while solving");
481 for (
int course = 0; course < model.courses_size(); ++course) {
482 for (
int section = 0; section < course_to_classes_[course].size();
484 ClassAssignment class_assignment;
485 class_assignment.set_course_index(course);
486 class_assignment.set_section_number(section);
487 const int class_index = course_to_classes_[course][section];
489 for (
int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
490 for (
int room = 0; room < room_count_; ++room) {
491 MPVariable* x_i = variables[class_index][time_slot][room];
492 if (x_i !=
nullptr && x_i->solution_value() == 1) {
493 if (solve_for_rooms_) {
494 class_assignment.add_room_indices(room);
496 class_assignment.add_time_slots(time_slot);
500 *result.add_class_assignments() = class_assignment;
508 LOG(INFO) <<
"Starting assign students solver.";
509 MPSolver mip_solver(
"AssignStudentsMIP",
515 std::vector<std::vector<MPVariable*>> variables(
516 model.students_size(), std::vector<MPVariable*>(class_count_,
nullptr));
517 for (
int student_index = 0; student_index < model.students_size();
519 const Student& student = model.students(student_index);
520 for (
const int course_index : student.course_indices()) {
521 for (
const int class_index : course_to_classes_[course_index]) {
522 variables[student_index][class_index] = mip_solver.MakeBoolVar(
523 absl::StrFormat(
"y_%d_%d", student_index, class_index));
530 for (
int student_index = 0; student_index < model.students_size();
532 const Student& student = model.students(student_index);
533 for (
const int course_index : student.course_indices()) {
534 MPConstraint*
const ct = mip_solver.MakeRowConstraint(1, 1);
535 for (
const int class_index : course_to_classes_[course_index]) {
536 AddVariableIfNonNull(1, variables[student_index][class_index], ct);
542 for (
int course_index = 0; course_index < model.courses_size();
544 const Course& course = model.courses(course_index);
545 const int min_cap = course.min_capacity();
546 const int max_cap = course.max_capacity();
547 for (
const int class_index : course_to_classes_[course_index]) {
548 MPConstraint*
const ct = mip_solver.MakeRowConstraint(min_cap, max_cap);
549 for (
int student = 0; student < model.students_size(); ++student) {
550 AddVariableIfNonNull(1, variables[student][class_index], ct);
558 std::vector<std::vector<MPVariable*>> infeasibility_vars(
559 model.students_size(),
560 std::vector<MPVariable*>(time_slot_count_,
nullptr));
561 std::vector<absl::flat_hash_set<int>> time_slot_to_classes =
562 GetClassesByTimeSlot(result);
563 for (
int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
564 for (
int student_index = 0; student_index < model.students_size();
566 infeasibility_vars[student_index][time_slot] = mip_solver.MakeIntVar(
568 absl::StrFormat(
"f_%d_%d", student_index, time_slot));
569 const Student& student = model.students(student_index);
571 MPConstraint*
const ct = mip_solver.MakeRowConstraint(0, 1);
572 ct->SetCoefficient(infeasibility_vars[student_index][time_slot], -1);
573 for (
const int course_index : student.course_indices()) {
574 for (
const int class_index : course_to_classes_[course_index]) {
575 if (!time_slot_to_classes[time_slot].contains(class_index))
continue;
577 AddVariableIfNonNull(1, variables[student_index][class_index], ct);
587 MPObjective*
const objective = mip_solver.MutableObjective();
588 for (
int student_index = 0; student_index < model.students_size();
590 for (
int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
591 objective->SetCoefficient(infeasibility_vars[student_index][time_slot],
596 mip_solver.SetSolverSpecificParametersAsString(
"limits/gap=0.01");
603 result->set_solver_status(MipStatusToCourseSchedulingResultStatus(status));
604 result->clear_class_assignments();
607 "Check the minimum or maximum capacity constraints for your "
611 return class_conflict_pairs;
614 LOG(INFO) <<
"Finished assign students solver with " << objective->Value()
615 <<
" schedule violations.";
616 if (objective->Value() > 0) {
617 for (
int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
618 for (
int student_index = 0; student_index < model.students_size();
620 std::vector<int> conflicting_classes;
621 MPVariable*
const f_i = infeasibility_vars[student_index][time_slot];
622 if (f_i !=
nullptr && f_i->solution_value() == 0)
continue;
624 for (
const int class_index : time_slot_to_classes[time_slot]) {
625 MPVariable*
const y_i = variables[student_index][class_index];
626 if (y_i !=
nullptr && y_i->solution_value() == 1) {
627 conflicting_classes.push_back(class_index);
630 InsertSortedPairs(conflicting_classes, &class_conflict_pairs);
633 return class_conflict_pairs;
636 for (
int student_index = 0; student_index < model.students_size();
638 StudentAssignment student_assignment;
639 student_assignment.set_student_index(student_index);
641 const Student& student = model.students(student_index);
642 for (
const int course_index : student.course_indices()) {
643 for (
int section_index = 0;
644 section_index < course_to_classes_[course_index].size();
646 int class_index = course_to_classes_[course_index][section_index];
647 MPVariable*
const y_i = variables[student_index][class_index];
649 if (y_i->solution_value() == 1) {
650 student_assignment.add_course_indices(course_index);
651 student_assignment.add_section_indices(section_index);
655 *result->add_student_assignments() = student_assignment;
658 return class_conflict_pairs;
662CourseSchedulingSolver::MipStatusToCourseSchedulingResultStatus(
664 switch (mip_status) {
685 std::vector<absl::flat_hash_set<int>> class_to_time_slots(class_count_);
686 std::vector<absl::flat_hash_set<int>> room_to_time_slots(model.
rooms_size());
688 const int course_index = class_assignment.course_index();
690 const int consecutive_time_slots =
694 if (class_assignment.time_slots_size() !=
695 meetings_count * consecutive_time_slots) {
696 return absl::InvalidArgumentError(absl::StrFormat(
697 "Verification failed: The course titled %s and section number %d "
698 "meets %d times when it should meet %d times.",
700 class_assignment.section_number(), class_assignment.time_slots_size(),
701 meetings_count * consecutive_time_slots));
704 const int class_index =
705 course_to_classes_[course_index][class_assignment.section_number()];
706 std::vector<std::vector<int>> day_to_time_slots(model.
days_count());
707 for (
const int time_slot : class_assignment.time_slots()) {
708 class_to_time_slots[class_index].insert(time_slot);
710 .push_back(time_slot);
716 for (
int day = 0; day < model.
days_count(); ++day) {
717 const int day_count = day_to_time_slots[day].size();
718 if (day_count != 0 && day_count != consecutive_time_slots) {
719 return absl::InvalidArgumentError(
720 absl::StrFormat(
"Verification failed: The course titled %s does "
721 "not meet the correct number of times in "
725 if (day_count != 2)
continue;
727 const int first_slot = day_to_time_slots[day][0];
728 const int second_slot = day_to_time_slots[day][1];
729 if (std::abs(first_slot - second_slot) != 1) {
730 return absl::InvalidArgumentError(
731 absl::StrFormat(
"Verification failed: The course titled %s is not "
732 "scheduled for consecutive time slots "
739 if (solve_for_rooms_) {
740 for (
int i = 0; i < class_assignment.room_indices_size(); ++i) {
741 const int room = class_assignment.room_indices(i);
742 const int time_slot = class_assignment.time_slots(i);
743 if (room_to_time_slots[room].contains(time_slot)) {
744 return absl::InvalidArgumentError(
745 absl::StrFormat(
"Verification failed: Multiple classes have "
746 "been assigned to room %s during time slot %d.",
749 room_to_time_slots[room].insert(time_slot);
756 for (
int teacher = 0; teacher < model.
teachers_size(); ++teacher) {
757 const auto& class_list = teacher_to_classes_[teacher];
758 absl::flat_hash_set<int> teacher_time_slots;
759 for (
const int class_index : class_list) {
760 for (
const int time_slot : class_to_time_slots[class_index]) {
761 if (teacher_to_restricted_slots_[teacher].contains(time_slot)) {
762 return absl::InvalidArgumentError(absl::StrFormat(
763 "Verification failed: Teacher with name %s has been assigned to "
764 "restricted time slot %d.",
767 if (teacher_time_slots.contains(time_slot)) {
768 return absl::InvalidArgumentError(absl::StrFormat(
769 "Verification failed: Teacher with name %s has been assigned to "
770 "multiple classes at time slot %d.",
773 teacher_time_slots.insert(time_slot);
778 std::vector<int> class_student_count(class_count_);
781 const int student_index = student_assignment.student_index();
784 std::vector<int> enrolled_courses =
787 std::vector<int> assigned_courses =
788 std::vector<int>(student_assignment.course_indices().begin(),
789 student_assignment.course_indices().end());
790 std::sort(enrolled_courses.begin(), enrolled_courses.end());
791 std::sort(assigned_courses.begin(), assigned_courses.end());
792 if (enrolled_courses != assigned_courses) {
793 return absl::InvalidArgumentError(
794 absl::StrFormat(
"Verification failed: Student with name %s has not "
795 "been assigned the correct courses.",
801 absl::flat_hash_set<int> student_time_slots;
802 for (
int i = 0; i < student_assignment.course_indices_size(); ++i) {
803 const int course_index = student_assignment.course_indices(i);
804 const int section = student_assignment.section_indices(i);
805 const int class_index = course_to_classes_[course_index][section];
806 ++class_student_count[class_index];
808 for (
const int time_slot : class_to_time_slots[class_index]) {
809 if (student_time_slots.contains(time_slot)) {
810 return absl::InvalidArgumentError(absl::StrFormat(
811 "Verification failed: Student with name %s has been assigned to "
812 "multiple classes at time slot %d.",
815 student_time_slots.insert(time_slot);
821 for (
int course = 0; course < model.
courses_size(); ++course) {
824 for (
const int class_index : course_to_classes_[course]) {
825 const int class_size = class_student_count[class_index];
826 if (class_size < min_cap) {
827 return absl::InvalidArgumentError(absl::StrFormat(
828 "Verification failed: The course titled %s has %d students when it "
829 "should have at least %d students.",
832 if (class_size > max_cap) {
833 return absl::InvalidArgumentError(absl::StrFormat(
834 "Verification failed: The course titled %s has %d students when it "
835 "should have no more than %d students.",
841 return absl::OkStatus();
const ::operations_research::Teacher & teachers(int index) const
const ::operations_research::Course & courses(int index) const
int students_size() const
const ::operations_research::Student & students(int index) const
::int32_t daily_time_slot_count() const
::int32_t days_count() const
int teachers_size() const
const ::operations_research::Room & rooms(int index) const
const ::operations_research::ClassAssignment & class_assignments(int index) const
void set_message(Arg_ &&arg, Args_... args)
void set_solver_status(::operations_research::CourseSchedulingResultStatus value)
const ::operations_research::StudentAssignment & student_assignments(int index) const
::operations_research::CourseSchedulingResultStatus solver_status() const
absl::flat_hash_set< std::pair< int, int > > ConflictPairs
virtual CourseSchedulingResult SolveModel(const CourseSchedulingModel &model, const ConflictPairs &class_conflicts)
virtual absl::Status ValidateModelAndLoadClasses(const CourseSchedulingModel &model)
virtual absl::Status VerifyCourseSchedulingResult(const CourseSchedulingModel &model, const CourseSchedulingResult &result)
CourseSchedulingResult Solve(const CourseSchedulingModel &model)
::int32_t min_capacity() const
::int32_t consecutive_slots_count() const
::int32_t room_indices(int index) const
int teacher_indices_size() const
::int32_t teacher_indices(int index) const
::int32_t meetings_count() const
::int32_t max_capacity() const
::int32_t teacher_section_counts(int index) const
const ::std::string & display_name() const
@ MODEL_INVALID
the model is trivially invalid (NaN coefficients, etc).
@ FEASIBLE
feasible, or stopped by limit.
@ NOT_SOLVED
not been solved yet.
@ INFEASIBLE
proven infeasible.
@ UNBOUNDED
proven unbounded.
@ ABNORMAL
abnormal, i.e., error of some kind.
@ SCIP_MIXED_INTEGER_PROGRAMMING
The class for variables of a Mathematical Programming (MP) model.
static T IPow(T base, int exp)
const ::std::string & display_name() const
::int32_t course_indices(int index) const
const ::std::string & display_name() const
::int32_t restricted_time_slots(int index) const
const ::std::string & display_name() const
static constexpr double kInfinity
ClosedInterval::Iterator end(ClosedInterval interval)
CourseSchedulingResultStatus
@ COURSE_SCHEDULING_RESULT_STATUS_UNSPECIFIED