Search code examples
prolog

Scheduling Exercise


I have to meet 6 project students in pairs and I have 3 slots in which I can do this, but there are some constraints.

Let's call the students student1 etc and the slots slot1 etc. A higher-numbered slot occurs after a lower-numbered slot.

The constraints are:

  1. student1 must meet in slot1.
  2. student2 and student3 must meet in the same slot.
  3. student1 and student4 cannot meet in the same slot.
  4. student6 cannot meet in slot1 or slot3.

I should meet each student exactly once.

I have written the following program to solve this scheduling puzzle:

student(student1).
student(student2).
student(student3).
student(student4).
student(student5).
student(student6).


slot(slot1).
slot(slot2).
slot(slot3).


constraint1(Slot1, _Slot2, _Slot3) :-
    member(student1-_, Slot1).

constraint2(Slot1, Slot2, Slot3) :-
    ( member(student2-_, Slot1), member(student3-_, Slot1) );
    ( member(student2-_, Slot2), member(student3-_, Slot2) );
    ( member(student2-_, Slot3), member(student3-_, Slot3) ).

constraint3(Slot1, Slot2, Slot3) :-
    \+ ( member(student1-_, Slot1), member(student4-_, Slot1) ),
    \+ ( member(student1-_, Slot2), member(student4-_, Slot2) ),
    \+ ( member(student1-_, Slot3), member(student4-_, Slot3) ).

constraint4(Slot1, _, Slot3) :-
    \+ member(student6-_, Slot1),
    \+ member(student6-_, Slot3).


meetings_one_two_three(Slot1, Slot2, Slot3) :-

    % Generate all possible assignments of students to slots.
    findall( (Slot1, Slot2, Slot3), (

        % Slot 1
        select_two_students(S1_1, S1_2, _Remaining1),
        Slot1 = [S1_1-S1_2, S1_2-S1_1], % Assign to slot 1.
    
        % Slot 2
        select_two_students(S2_1, S2_2, _Remaining2),
        Slot2 = [S2_1-S2_2, S2_2-S2_1], % Assign to slot 2.

        % Slot 3
        select_two_students(S3_1, S3_2, _Remaining3),
        Slot3 = [S3_1-S3_2, S3_2-S3_1], % Assign to slot 3.

        all_different([S1_1, S1_2, S2_1, S2_2, S3_1, S3_2]), % All students must be different.

        constraint1(Slot1, Slot2, Slot3),
        constraint2(Slot1, Slot2, Slot3),
        constraint3(Slot1, Slot2, Slot3),
        constraint4(Slot1, Slot2, Slot3)

    ), Solutions),

    member( (Slot1, Slot2, Slot3), Solutions). % Select one solution.


select_two_students(S1, S2, Remaining) :-
    student(S1),
    student(S2),
    S1 @< S2,    % Ensure we do not generate (A, B) and (B, A) which is essential for efficiency. This condition reduces the search space.
    findall(Other, (student(Other), Other \= S1, Other \= S2), Remaining).


all_different([]).
all_different([H|T]) :-
    \+ member(H,T),
    all_different(T).

When I consult and query the knowledge base and input the following at the Prolog prompt, I receive these results:

?- consult(scheduling).
true.

?- meetings_one_two_three(Slot1, Slot2, Slot3).
Slot1 = [student1-student5, student5-student1],
Slot2 = [student4-student6, student6-student4],
Slot3 = [student2-student3, student3-student2].

This is correct, but the S1 @< S2 should have prevented the generation of both student1-student5 and student5-student1, for example.

I, then, realized that the problem must be the following three lines:

Slot1 = [S1_1-S1_2, S1_2-S1_1], % Assign to slot 1.
Slot2 = [S2_1-S2_2, S2_2-S2_1], % Assign to slot 2.
Slot3 = [S3_1-S3_2, S3_2-S3_1], % Assign to slot 3.

So, I modified them to the following:

Slot1 = [S1_1-S1_2], % Assign to slot 1.
Slot2 = [S2_1-S2_2], % Assign to slot 2.
Slot3 = [S3_1-S3_2], % Assign to slot 3.

I was confident this would fix the issue but unfortunately I get false.

What am I missing? Could someone please help me understand what I am doing wrong and how I can resolve it with minimal changes?


Solution

  • member is too simplistic, and therefore slow/inelegant - want select or actually a more flexible, custom variant of select:

    sched_slots(Slots) :-
        % Create ordered list - and do not scramble the order
        numlist(1, 6, Vars),
        % student1 must meet in slot1
        Slots = [s(1, S1Pos2), _, s(S3Pos1, S3Pos2)|_],
        Not6 = [S1Pos2, S3Pos1, S3Pos2],
        assign_slots(Vars, Slots),
        % student2 and student3 must meet in the same slot
        memberchk(s(2, 3), Slots),
        % student1 and student4 cannot meet in the same slot
        \+ memberchk(s(1, 4), Slots),
        % student6 cannot meet in slot1 or slot3
        \+ memberchk(6, Not6).
    
    assign_slots([], []).
    assign_slots([H|T], [s(Lower, Upper)|Slots]) :-
        grab_2([H|T], Lower, Upper, Rem),
        assign_slots(Rem, Slots).
    
    grab_2(Vars, Lower, Upper, Rem) :-
        select_forward_remainder_prev_tail(Lower, Vars, Forw, _, Rem, PrevTail),
        % Preventing slow append by unifying PrevTail
        select_forward_remainder_prev_tail(Upper, Forw, _, PrevTail, _, _).
    
    % Based on select/3
    select_forward_remainder_prev_tail(E, [H|T], F, R, P, PT) :-
        select_forward_remainder_prev_tail_(T, H, E, F, R, P, PT).
    
    select_forward_remainder_prev_tail_(T, H, H, T, T, PT, PT).
    select_forward_remainder_prev_tail_([H|T], A, E, F, [A|R], [A|P], PT) :-
        select_forward_remainder_prev_tail_(T, H, E, F, R, P, PT).
    

    Result in swi-prolog:

    ?- time(sched_slots(Slots)).
    % 362 inferences, 0.000 CPU in 0.000 seconds (95% CPU, 3766086 Lips)
    Slots = [s(1, 5), s(4, 6), s(2, 3)] ;
    % 108 inferences, 0.000 CPU in 0.000 seconds (91% CPU, 3348733 Lips)
    false.
    

    A demonstration of select_forward_remainder_prev_tail:

    ?- select_forward_remainder_prev_tail(E, [1,2,3,4], F, R, P, T).
    E = 1,
    F = R, R = [2, 3, 4],
    P = T ;
    E = 2,
    F = [3, 4],
    R = [1, 3, 4],
    P = [1|T] ;
    E = 3,
    F = [4],
    R = [1, 2, 4],
    P = [1, 2|T] ;
    E = 4,
    F = [],
    R = [1, 2, 3],
    P = [1, 2, 3|T].