0

As my first Prolog challenge I'm trying to solve this puzzle:

There are eight floors in an apartment building. The fifth floor has the only apartment with two bedrooms. Mrs Barber has a baby and cannot carry a pram upstairs when the lift is out of order. Mr and Mrs Elder also find climbing difficult now that they’re retired. Mr Archer likes the peace and quiet of living on the top floor. Mrs Cook and her daughter need a two bedroom flat. Mr and Mrs Hooper live just below Mr Archer. Mrs Cook lives above Mr Gardner and below Mrs Driver. Mr and Mrs Fisher live above Mr and Mrs Elder.

Essentially, I see it as a set of algebraic integer relations, so I'd like to instantiate each entity as an integer in [1..8] and declare the known values and relationships.

floors([Archer,Barber,Cook,Driver,Elder,Fisher,Gardner,Hooper]) :-
    Archer=8,
    Cook=5,
    Hooper is Archer-1,
    Barber<4,
    Elder<4,
    Fisher>Elder,
    Cook>Gardner,
    Cook<Driver.
?- floors([A,B,C,D,E,F,G,H]).

However I get ERROR:Arguments are not sufficiently instantiated at line 5 (the first inequality).

I tried adding

Vars = [Archer,Barber,Cook,Driver,Elder,Fisher,Gardner,Hooper],
Vars ins 1..8,

But then I get the vague ERROR: Unknown procedure: floors/1 (DWIM could not correct goal)

I'm sure I'm missing something obvious. All help appreciated.

false
  • 10,264
  • 13
  • 101
  • 209
Josh Friedlander
  • 10,870
  • 5
  • 35
  • 75

3 Answers3

3

I like to take a structural view of these kinds of puzzles. I would start out with this:

[floor(1,[_]),floor(2,[_]),floor(3,[_]),floor(4,[_]),floor(5,[_,_]),floor(6,[_]),floor(7,[_]),floor(8,[_])]

Note that I have used a list of anonymous variables to represent the bedrooms on each floor. Floor 5 is the only one with two bedrooms.

Now I need to define a bunch of helper predicates.

onfloor(N,X,Floors) :-
    member(floor(N,Xs),Floors),
    member(X,Xs).

justbelow(X,Y,[floor(_,Xs),floor(_,Ys)|_]) :-
    member(X,Xs),
    member(Y,Ys).
    
justbelow(X,Y,[_|T]) :-
    justbelow(X,Y,T).
    
below(X,Y,Floors) :-
    append(Lower,Upper,Floors),
    onfloor(_,X,Lower),
    onfloor(_,Y,Upper). 

lower3([A,B,C|_],[A,B,C]).

top([T],[T]).
top([_|T],X) :- top(T,X).

Now my sovle(Floors) predicate is a simple translation of each of the clues:

solve(Floors) :-
    Floors = [floor(1,[_]),floor(2,[_]),floor(3,[_]),floor(4,[_]),floor(5,[_,_]),floor(6,[_]),floor(7,[_]),floor(8,[_])],
    lower3(Floors,Lower),
    onfloor(_,barber,Lower),
    onfloor(_,elder,Lower),
    top(Floors,Top),
    onfloor(_,archer,Top),
    onfloor(Floor,cook,Floors),
    onfloor(Floor,daughter,Floors),
    justbelow(hooper,archer,Floors),
    below(gardener,cook,Floors),
    below(cook,driver,Floors),
    below(elder,fisher,Floors),
    true.

When I run this with ?- solve(Floors). I get the following:

floor(1,[barber]),
floor(2,[elder]),
floor(3,[gardener]),
floor(4,[fisher]),
floor(5,[cook,daughter]),
floor(6,[driver]),
floor(7,[hooper]),
floor(8,[archer])
Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • 1
    Thanks, I like how this maps the natural language so directly to Prolog functions. (For the record I was later told that "just below" and "below" both should be taken as "just below", such that there is only one unique solution. It's a badly set riddle...) – Josh Friedlander Aug 27 '20 at 09:53
1

I'd assumed that using integers rather than FloorOne, FloorTwo, etc. would be simpler. However, reasoning about integers in Prolog requires a special library called CLP(FD) (or CLP(ℤ)), which stands for Constraint Logic Programming over Finite Domains, which works slightly differently from the base case of reasoning about facts. (Thanks to false for the tip - for more on this see here.) In SWI-Prolog this must be imported explicitly. The possibility space in my example is all the permuatations of residents and floor numbers, and the facts constrain the space of permitted permutations.

% import a constraint-solving library
% note that the syntax for clpfd has a hash sign (#=, #<, etc)
:- use_module(library(clpfd)).

floors([Archer,Barber,Cook,Driver,Elder,Fisher,Gardner,Hooper]) :-
    % possible facts are all the permutations
  permutation( [Archer, Barber, Cook, Driver, Elder, Fisher, Gardner, Hooper], [1, 2, 3, 4, 5, 6, 7, 8]),
    % clues
  Archer #= 8,
  Cook #= 5,
  Hooper is Archer - 1,
  Barber #= 1,
  Elder #= 2,
  Fisher #> Elder,
  Cook #> Gardner,
  Cook #< Driver.

?- floors([A,B,C,D,E,F,G,H]).
A = 8,
B = 1,
C = 5,
D = 6,
E = 2,
F = 3,
G = 4,
H = 7 ;
A = 8,
B = 1,
C = 5,
D = 6,
E = 2,
F = 4,
G = 3,
H = 7
Josh Friedlander
  • 10,870
  • 5
  • 35
  • 75
1

The reason for the error message in your original code is the following: When you use an operator like < as in Barber<4, the variable Barber must have been instantiated before. (Think about the alternative, namely that prolog should pick one value for Barber to satisfy the goal 'Barber<4': The number of possibilities to instantiate Barber would be enormous.) There is no goal, however, before Barber<4 that forces prolog to instantiate Barber.

You have solved this problem in your own answer to your question by adding the goal

permutation([Archer, Barber, Cook, Driver, Elder, Fisher, Gardner, Hooper], [1, 2, 3, 4, 5, 6, 7, 8])

because this goal forces each of the variables Archer .. Hooper to be instantiated with one of the numbers. With this approach, you could just have used the original operators without '#', because the original error message about insufficiently instantiated variables would no longer be issued.

When it comes to instantiating integer variables, there is another predicate that could be helpful for you: between/3, which could be used as follows within your code: ... between(1,8,Barber), Barber<4, ... . The goal between(1,8,Barber) will ensure that Barber gets instantiated with a value from 1..8 before the goal Barber<4 is considered.

Certainly, in this particular case you could combine the two goals between(1,8,Barber), Barber<4 to one goal between(1,3,Barber). Thus, the complete example could also have been realized with between/3 instead of permutation/2 in the following way:

:- use_module(library(clpfd)).
floors([Archer,Barber,Cook,Driver,Elder,Fisher,Gardner,Hooper]) :-
    Archer=8,
    Cook=5,
    Hooper is Archer-1,
    between(1,3,Barber),
    between(1,3,Elder),
    AboveElder is Elder+1,
    between(AboveElder,8,Fisher),
    BelowCook is Cook-1,
    between(1,BelowCook,Gardner),
    AboveCook is Cook+1,
    between(AboveCook,8,Driver),
    all_different([Archer,Barber,Cook,Driver,Elder,Fisher,Gardner,Hooper]).
Dirk Herrmann
  • 5,550
  • 1
  • 21
  • 47