Assignment to groups and scheduling: how to improve constraints?

239 views
Skip to first unread message

Didier Lesesvre

unread,
Aug 28, 2022, 10:46:18 PM8/28/22
to MiniZinc
Hi everyone,

I got into MiniZinc some weeks ago and am super fond of it. My main purpose was to solve a pretty annoying scheduling problem my department faces each year, spending weeks to solve it (partially). I tride to solve this in MiniZinc, but run into some complexity (?) issues.

Here is the main setting:

- I have a certain number of students
- They rank 7 topics in order of preference
- They will be assigned 4 of such topics
- The groups have a minimal/maximal size (but there may be various groups on the same topic)
- There is 4 time slots available in the timetable for all of these

I need to... get something workable out of it. The most obvious constraints are:

- students cannot attend two topics at the same time
- various topics groups cannot be at the same time (only one teacher)
- we would like to maximize the satisfaction of the problem (but in fact, it would be already pretty good to have just a possible situation)

After many trials and errors, and some insights from colleagues of the Coursera course on MiniZinc, I ended up having a (barely working) model to assign each student to a topic, but of course this neglects the true scheduling problem (compatibility for both students and teachers, even though it would work with enough teachers).

I would like to know if I am missing obivous extra structures or constraints I may add to speed it up enough to get an answer.

Here is the current model:

==============

% PARAMETERS

int: n; % number of students
set of int: STUDENTS=1..n;

int: m; % number of topics
int: g=5; % number of instances of each course
set of int: PREFERENCE=1..m;
set of int: TOPICS=1..g*m; % different topics (with instances)

int: c; % number of courses each student will take

int: maxseats; % number of seats available per course max
int: minseats; % number of seats minimum per course

int: timeslots; % possible time slots for the courses 
set of int: TIMESLOTS=0..timeslots; 

array[STUDENTS,1..m] of PREFERENCE: preference; % ranking of topics by each student
array[STUDENTS, PREFERENCE] of TOPICS: prefRank = array2d(STUDENTS, PREFERENCE, [rank | student in STUDENTS, p in 1..m, rank in 1..m where preference[student, rank] == p]);

array[STUDENTS,1..c] of var PREFERENCE: courseRank;
constraint % channeling constraint to obtain topic ranking
forall(student in STUDENTS, i in 1..c)(courseRank[student, i] = prefRank[student, topicToTopic(course[student, i])]);

% VARIABLES

array[STUDENTS,1..c] of var TOPICS: course; % topics assigned to each student
array[TOPICS] of var TIMESLOTS: schedule; % time slot assigned to topics 

array[TOPICS] of var 0..maxseats: number; % number of people taking each course
constraint forall (topic in TOPICS)(number[topic] = sum(student in STUDENTS, i in 1..c)(course[student,i] == topic));

% FUNCTIONS (to navigate between istances and topics)

function int: topicToGroupFix(var int: t) = fix(((t-1) mod g) + 1);
function var int: topicToTopic(var int: t) = ((t-1) div g) + 1;
function int: topicToTopicFix(var int: t) = fix(((t-1) div g) + 1); 
function int: topicGroupToTopic(int: t, int: gg) = ((t-1) * g) + gg;

% CONSTRAINTS

include "globals.mzn"; 

constraint % before using another topic group make sure the previous one has students in it
forall(t in 1..m, gg in 1..g-1)( number[topicGroupToTopic(t,gg+1)] > 0 -> number[topicGroupToTopic(t,gg)] >= minseats); 

constraint % if students are assigned to a topic group it must have enough
forall(t in TOPICS)( number[t] > 0 -> number[t] >= minseats ); 

constraint % make sure topics are slotted at different times
forall(t in 1..m)( alldifferent_except_0([schedule[x] | x in topicGroupToTopic(t,1)..topicGroupToTopic(t,g)])); 

constraint % no student has two topics at the same time
forall(student in STUDENTS)(alldifferent([schedule[course[student,i]] | i in 1..c])); 

constraint % students have different topics (and break symmetries)
forall(student in STUDENTS)(strictly_increasing([courseRank[student, i] | i in 1..c]));

constraint % students have different topics
forall(student in STUDENTS)
(alldifferent([topicToTopic(course[student, i]) | i in 1..c])); 

constraint % fairness - don't want any one student to get a bad schedule relative to others
forall(student in STUDENTS)(sum([courseRank[student,i] | i in 1..c]) <= min([sum([courseRank[s,i] | i in 1..c]) | s in STUDENTS where s != student])+1);

constraint % fairness - don't a student get a much worse choice than others
forall(student in STUDENTS)(courseRank[student,c] <= min([courseRank[s,c] | s in STUDENTS where s != student])+1);

% break symmetries on the schedule
constraint
forall(t in TOPICS)(if number[t] == 0 then schedule[t] == 0 else schedule[t] >= 1 /\ schedule[t] <= t endif);

constraint
forall(t in 1..m*g-1)(schedule[t+1] <= (max(x in TOPICS)(schedule[x])+1))

solve satisfy; (or maximize a certain satisfaction)

==============

The datafile is attached, it is far too slow (even hours aren't enough) even for c=2 (i.e. assigning two topics per student), so c=4 will be a nightmare.

I am modelling the different instances of the same topic as different topics essentially (with an extra alldifferent constraint, since there is only one teacher per topic). I am not fond of this solution and maybe there is a more clever way to handle it.

I also tried with some boolean variables instead, and mainly the solvers gecode, chuffed and coin-bc. I wonder if there is a more elegant way of defining these variables (declaring them as equivalent/inverse to some others, even if it is only an injective map).

Do you have a more clever approach to it? Any help would be very welcome!

Best wishes,
Didier

data3.dzn
Reply all
Reply to author
Forward
0 new messages