Решение текстовой логической головоломки в Prolog - Найдите день рождения и месяц

Я читаю книгу «7 языков за 7 дней» и дошел до главы «Пролог». В качестве обучающих упражнений я пытаюсь решить некоторые текстовые логические головоломки. Головоломка выглядит следующим образом:

У пяти сестер день рождения в разном месяце и у каждой в разный день недели. Используя подсказки ниже, определите месяц и день недели, когда у каждой сестры день рождения.

  1. Паула родилась в марте, а не в субботу. День рождения Абигейл был не в пятницу и не в среду.
  2. Девушка, у которой день рождения в понедельник, родилась раньше в этом году, чем Бренда и Мэри.
  3. Тара родилась не в феврале, и ее день рождения был в выходные.
  4. Мэри родилась не в декабре, а ее день рождения не был в будний день. В воскресенье родилась девочка, чей день рождения был в июне.
  5. Тара родилась раньше Бренды, чей день рождения пришелся на пятницу. Мэри родилась не в июле.

Моя текущая реализация, вероятно, выглядит как шутка для опытных программистов на Прологе. Код вставлен ниже.

Я хотел бы получить информацию о том, как решить этот вопрос и как сделать код четким и плотным.

Ie:

  1. Как мне не указывать ограничения, говорящие о том, что Дни должны быть уникальными.
  2. Как я могу не указывать ограничения, в которых говорится, что месяцы должны быть уникальными.
  3. Добавьте ограничение на порядок дней рождения.
is_day(Day) :-
    member(Day, [sunday, monday, wednesday, friday, saturday]).

is_month(Month) :-
    member(Month, [february, march, june, july, december]).

solve(S) :-

    S = [[Name1, Month1, Day1],
         [Name2, Month2, Day2],
         [Name3, Month3, Day3],
         [Name4, Month4, Day4],
         [Name5, Month5, Day5]],

    % Five girls; Abigail, Brenda, Mary, Paula, Tara    
    Name1 = abigail,
    Name2 = brenda,
    Name3 = mary,
    Name4 = paula,
    Name5 = tara,

    is_day(Day1), is_day(Day2), is_day(Day3), is_day(Day4), is_day(Day5),
    Day1 \== Day2, Day1 \== Day3, Day1 \== Day4, Day1 \== Day5,
    Day2 \== Day1, Day2 \== Day3, Day2 \== Day4, Day2 \== Day5,
    Day3 \== Day1, Day3 \== Day2, Day3 \== Day4, Day3 \== Day5,
    Day4 \== Day1, Day4 \== Day2, Day4 \== Day3, Day4 \== Day5,

    is_month(Month1), is_month(Month2), is_month(Month3), is_month(Month4), is_month(Month5),
    Month1 \== Month2, Month1 \== Month3, Month1 \== Month4, Month1 \== Month5,
    Month2 \== Month1, Month2 \== Month3, Month2 \== Month4, Month2 \== Month5,
    Month3 \== Month1, Month3 \== Month2, Month3 \== Month4, Month3 \== Month5,
    Month4 \== Month1, Month4 \== Month2, Month4 \== Month3, Month4 \== Month5,

    % Paula was born in March but not on Saturday.  
    member([paula, march, _], S),
    Day4 \== sunday,

    % Abigail's birthday was not on Friday or Wednesday.    
    Day1 \== friday,
    Day1 \== wednesday,

    % The girl whose birthday is on Monday was born
    % earlier in the year than Brenda and Mary.

    % Tara wasn't born in February, and 
    % her birthday was on the weekend.
    Month5 \== february,
    Day5 \== monday, Day5 \== wednesday, Day5 \== friday,   

    % Mary was not born in December nor was her
    % birthday on a weekday.
    Month3 \== december,
    Day3 \== monday, Day3 \== wednesday, Day3 \== friday,

    % The girl whose birthday was in June was 
    % born on Sunday.
    member([_, june, sunday], S),

    % Tara was born before Brenda, whose birthday
    % wasn't on Friday.
    Day2 \== friday,

    % Mary wasn't born in July.
    Month3 \== july.

Обновление На основании ответа от chac мне удалось решить загадку. Следуя тому же рецепту, мы (рабочая группа по изучению языков программирования) смогли решить и вторую головоломку. Я опубликовал завершенную реализацию и пример вывода как суть на GitHub.


person Jonas Follesø    schedule 03.09.2012    source источник


Ответы (6)


Может быть, загадка недооценена, или ваше решение не завершено: проверяю ваш код, я получаю

?- solve(X),maplist(writeln,X).
[abigail,february,monday]
[brenda,july,wednesday]
[mary,june,sunday]
[paula,march,friday]
[tara,december,saturday]
X = [[abigail, february, monday], [brenda, july, wednesday], [mary, june, sunday], [paula, march, friday], [tara, december, saturday]] ;
[abigail,february,monday]
[brenda,december,wednesday]
[mary,june,sunday]
[paula,march,friday]
[tara,july,saturday]
X = [[abigail, february, monday], [brenda, december, wednesday], [mary, june, sunday], [paula, march, friday], [tara, july, saturday]] 

и еще несколько решений. Так когда же родится Бренда?

Уловка уникальности заключается в использовании select / 3 или просто перестановка / 2. Используя это последнее, код становится чем-то вроде

solve(S) :-

    S = [[Name1, Month1, Day1],
         [Name2, Month2, Day2],
         [Name3, Month3, Day3],
         [Name4, Month4, Day4],
         [Name5, Month5, Day5]],

    Girls =  [abigail, brenda, mary, paula, tara],
    Girls =  [Name1, Name2, Name3, Name4, Name5],

    Months = [february, march, june, july, december],
    Days =   [sunday, monday, wednesday, friday, saturday],
    permutation(Months, [Month1, Month2, Month3, Month4, Month5]),
    permutation(Days,   [Day1, Day2, Day3, Day4, Day5]),

    % Paula was born in March but not on Saturday.
    member([paula, march, C1], S), C1 \= saturday,
   ...

отношение к «раньше в году» можно закодировать следующим образом:

    ...
    % The girl whose birthday is on Monday was born
    % earlier in the year than Brenda and Mary.
    member([_, C3, monday], S),
    member([brenda, C4, C10], S), before_in_year(C3, C4, Months),
    member([mary, C5, _], S), before_in_year(C3, C5, Months),
    ...

с предикатом службы

before_in_year(X, Y, Months) :-
    nth1(Xi, Months, X),
    nth1(Yi, Months, Y),
    Xi < Yi.

«Рожденные в выходные» можно закодировать как

...
% Tara wasn't born in February, and
% her birthday was on the weekend.
member([tara, C6, C7], S), C6 \= february, (C7 = saturday ; C7 = sunday),

% Mary was not born in December nor was her
% birthday on a weekday.
member([mary, C8, C9], S), C8 \= december, (C9 = saturday ; C9 = sunday),
...

и так далее. После этого переписывания я получаю уникальное решение

?- solve(X),maplist(writeln,X).
[abigail,february,monday]
[brenda,december,wednesday]
[mary,june,sunday]
[paula,march,friday]
[tara,july,saturday]
X = [[abigail, february, monday], [brenda, december, wednesday], [mary, june, sunday], [paula, march, friday], [tara, july, saturday]] ;
false.

изменить

Я только что заметил, что ввел некоторые избыточные переменные member / 2 и free, например member([brenda, C4, C10], S),.... Эти C4, C10 обычно могут быть заменены переменными, привязанными к Бренде как Month2, Day2, как это было в исходном коде.

person CapelliC    schedule 04.09.2012
comment
Это просто гениально - круто! Мне удалось получить 4 ответа самостоятельно - спецификация, которую я не мог выразить, была before_in_year. Кроме того, большое спасибо за совет permutation / 2 и за совет о том, как форсировать месяцы / дни. - person Jonas Follesø; 04.09.2012

Использование maplist / 2 значительно сократит ваш код. Например:

maplist(is_month, [Month1,Month2,Month3,Month4,Month5]).

месяц / 1 может быть лучшим именем предиката, чем is_month / 1. Чтобы указать, что два термина различны, используйте ограничение dif / 2. Используя maplist / 2 и dif / 2, вы можете описать, что список содержит попарно различные элементы:

all_dif([]).
all_dif([L|Ls]) :-
        maplist(dif(L), Ls),
        all_dif(Ls).

Пример:

?- all_dif([X,Y,Z]).
dif(X, Z),
dif(X, Y),
dif(Y, Z).

решить / 1 - это императивное имя - вы описываете решения, поэтому лучше называть его решением / 1.

person mat    schedule 03.09.2012
comment
Большое спасибо! Предикат all_dif + список карт значительно помог сократить код. Кроме того, спасибо за информацию о том, как сделать код более идоматическим, сделав предикатные слова более повествовательными и менее императивными! - person Jonas Follesø; 04.09.2012
comment
Я заметил, что вы наконец-то зарегистрировали аккаунт. Поздравляю. Вы бы хотели, чтобы мы объединили все ваши старые учетные записи в эту? - person Robert Harvey; 01.10.2012
comment
Да, пожалуйста, объедините все мои существующие в настоящее время учетные записи. Спасибо! - person mat; 03.10.2012

Вот решение, которое использует поиск методом перебора в проблемном пространстве. Сказать, что я не горжусь этим, будет недостаточно. Наверняка есть более элегантное решение этой проблемы.

Так или иначе:

month(january).
month(february).
month(march).
month(april).
month(may).
month(june).
month(july).
month(august).
month(september).
month(october).
month(november).
month(december).

precedes(january, february).
precedes(february, march).
precedes(march, april).
precedes(april, may).
precedes(may, june).
precedes(june, july).
precedes(july, august).
precedes(august, september).
precedes(september, october).
precedes(october, november).
precedes(november, december).
earlier(M1, M2) :- precedes(M1, M2).
earlier(M1, M2) :- month(M1), month(M2), precedes(M1, X), month(X), earlier(X, M2).

weekday(monday).
weekday(tuesday).
weekday(wednesday).
weekday(thursday).
weekday(friday).
weekend(saturday).
weekend(sunday).

birthmonth(abigail, M) :- 
    month(M), 
    M \== march.
birthmonth(brenda, M) :- 
    month(M), 
    M \== march.
birthmonth(paula, march).
birthmonth(mary, M) :- 
    month(M), 
    M \== march, M \== december, M \== july.
birthmonth(tara, M) :- 
    month(M), 
    M \== march, 
    M \== february.

birthday(abigail, D) :- 
    weekday(D), 
    D \== friday, D \== wednesday.
birthday(brenda, D) :- 
    weekday(D), 
    D \== friday,
    D \== monday.
birthday(mary, D) :- weekend(D).
birthday(paula, D) :- weekday(D), D \==saturday.
birthday(tara, D) :- weekend(D).

answer(M, D):-
    candidate(M, D),
    member(june, M),
    member(sunday, D),
    nth(IM, M, june),
    nth(ID, D, sunday),
    IM =:= ID,
    nth(5, M, MTARA),
    nth(2, M, MBRENDA),
    earlier(MTARA, MBRENDA),
    nth(3, M, MMARY),
    nth(IMONDAY, D, monday),
    nth(IMONDAY, M, MMONDAY),
    earlier(MMONDAY, MBRENDA),
    earlier(MMONDAY, MMARY).


candidate([M1,M2,M3,M4,M5], [D1,D2,D3,D4,D5]):-
    birthday(abigail, D1),
    birthday(brenda, D2),
    D1 \== D2,
    birthday(mary, D3),
    D1 \== D3,
    D2 \== D3,
    birthday(paula, D4),
    D1 \== D4,
    D2 \== D4,
    D3 \== D4,
    birthday(tara, D5),
    D1 \== D5,
    D2 \== D5,
    D3 \== D5,
    D4 \== D5,
    birthmonth(abigail, M1), 
    birthmonth(brenda, M2), 
    M1 \== M2,
    birthmonth(mary, M3), 
    M1 \== M3, 
    M2 \== M3,
    birthmonth(paula, M4),
    M1 \== M4,
    M2 \== M4,
    M3 \== M4,
    birthmonth(tara, M5),
    M1 \== M5,   
    M2 \== M5,
    M3 \== M5,
    M4 \== M5.

Лучшим ответом было бы реализовать ограничения порядка как часть предложений birthmonth/2 или birthday/2. Мне пока не удалось заставить это работать.

candidate/2 реализует то, что составляет пару вложенных for() циклов, которые вы не видите, но WAM (абстрактная машина Уоррена Пролога) выполняет махинации для перебора значений _6 _... и т. Д.

Чтобы увидеть возможные ответы, используйте:

answer(M,D).

Продолжайте нажимать точку с запятой или «a» в gprolog, чтобы увидеть все ответы. Элементы каждого списка соответствуют девушкам в алфавитном порядке.

person Heath Hunnicutt    schedule 04.09.2012
comment
@ Уилл Несс - Я не думаю, что есть основания полагать, что решения должны быть ограничены таким образом. - person Heath Hunnicutt; 04.09.2012
comment
@Will Ness - я бы предпочел программу, которая производит несколько возможных решений, но не добавляет никаких неустановленных условий. Как уже говорилось, исходная проблема имеет несколько решений, а не единственное. Это может быть неудачный пример проблемы, но пусть будет так. Вы бы наняли человека, который возьмется на себя исключить возможные решения, притворившись, что только те месяцы, которые были упомянуты в ограничениях, являются возможными решениями? Если бы это был вопрос интервью, я бы счел это fail. - person Heath Hunnicutt; 05.09.2012
comment
Решили проблему с головоломкой дня рождения и ограничили ее указанными днями и месяцами. мы выбрали эту головоломку, поскольку она объясняется в примере, показанном на puzzlersparadise.com/article1021.html как использовать таблицы для решения таких головоломок. Таким образом, в таблице уже были перечислены возможные значения. См. Другой пример, посвященный головоломке плей-офф футбола (gist.github.com/3622877). В нем использовались только заявленные факты (названия клубов, должности, имена и фамилии) и было дано единственное решение после заполнения подсказок. - person Jonas Follesø; 05.09.2012

В задачах такого рода мне нравится следовать тексту головоломки (работает с SWI Prolog 6.3.0):

week_end(Day) :-
    member(Day, [saturday, sunday]).

day(Day) :-
    member(Day, [monday, wednesday, friday, saturday, sunday]).

month(Month) :-
    member(Month, [february, march, june, july, december]).


before(M1, M2) :-
    nth0(I1, [february, march, june, july, december], M1),
    nth0(I2, [february, march, june, july, december], M2),
    I1 < I2.

names([person(abigail, _, _),
       person(brenda, _, _),
       person(mary, _, _),
       person(paula, _, _),
       person(tara, _, _)]).


solve(L) :-
    maplist(\X^(X = person(_, Day, Month),
            day(Day),
            month(Month)),
        L),

    forall((select(X,L, L1), select(Y, L1, _)),
           (   X = person(_, D1, M1),
           Y = person(_, D2, M2),
           D1 \= D2,
           M1 \= M2)).

/*
1.Paula was born in March but not on Saturday. Abigail's birthday was not on Friday or Wednesday.
*/
rule_1(L) :-
    member(person(paula, D, march), L),
        D \== saturday,

    member(person(abigail, D1, _M), L),
    day(D1),
    \+ member(D1, [friday, wednesday]).


/*
2.The girl whose birthday is on Monday was born earlier in the year than Brenda and Mary.
*/
rule_2(L) :-
    member(person(_N, monday, M), L),
    member(person(brenda, _D1, M1), L),
    member(person(mary, _D2, M2), L),
    before(M, M1),
    before(M, M2).

/*
3.Tara wasn't born in February and her birthday was on the weekend.
*/

rule_3(L) :-
    member(person(tara, D, M), L),
    M \== february,
    week_end(D).

/*
4.Mary was not born in December nor was her birthday on a weekday. The girl whose birthday was in June was born on Sunday.
*/

rule_4(L) :-
    member(person(mary, D, M), L),
    week_end(D),
    M \== december,
    member(person(_N, sunday, june), L).

/*
5.Tara was born before Brenda, whose birthday wasn't on Friday. Mary wasn't born in July.
*/

rule_5(L) :-
    member(person(tara, _DT, MT), L),
    member(person(brenda, DB, MB), L),
    before(MT, MB),
    % DB \== friday,
    day(DB),
    DB \= friday,    
    member(person(mary, _D, M), L),
    M \== july.



puzzle :-
    names(L),
    rule_1(L),
    rule_2(L),
    rule_3(L),
    rule_4(L),
    rule_5(L),
    solve(L),
    maplist(writeln, L).

Я получил :

 ?- time(puzzle).
person(abigail,monday,february)
person(brenda,wednesday,december)
person(mary,sunday,june)
person(paula,friday,march)
person(tara,saturday,july)
% 45,144 inferences, 0.016 CPU in 0.031 seconds (50% CPU, 3294080 Lips)
true .
person joel76    schedule 04.09.2012

Уникальный выбор всех сущностей заранее из домена позволяет получить легкий и простой, «одновременно ясный и плотный» код. Использование числовых доменов упрощает сравнение:

day(   d(_,D,_), D).   
fname( d(N,_,_), N).   % first name
month( d(_,_,M), M).   

sistersP(X):-
    maplist( fname, X, ['Paula', 'Abigail', 'Brenda', 'Mary', 'Tara']),
    maplist( month, X, [PM, AM, BM, MM, TM]),
    maplist( day,   X, [PD, AD, BD, MD, TD]),
    permutation( [PM,AM,BM,MM,TM], [2,3,6,7,12]),            % months of year
    permutation( [PD,AD,BD,MD,TD], [sun,mon,wed,fri,sat]),   % days of week

    PM = 3, PD \== sat, AD \== fri, AD \== wed,              % the five rules,
    day(G,mon), member(G,X), month(G,GM), GM < BM, GM < MM,  %   one per line
    TM =\= 2, (TD == sat ; TD == sun),
    MM =\= 12, (MD == sat ; MD == sun), month(G2,6), day(G2,sun), member(G2,X),
    TM < BM, BD \== fri, MM =\= 7.

Это находит только одно решение, используя только те месяцы года и дни недели, которые упомянуты в головоломке:

?- sistersP(X).
X = [d('Paula', fri, 3), d('Abigail', mon, 2), d('Brenda', wed, 12), 
     d('Mary', sun, 6), d('Tara', sat, 7)] ;
No

?- time( sistersP(_) ).
% 19,537 inferences, 0.01 CPU in 0.01 seconds (100% CPU, 2624221 Lips)
Yes

?- time( (sistersP(_),fail;true) ).  % exhaust the search space
% 56,664 inferences, 0.03 CPU in 0.04 seconds (75% CPU, 2441285 Lips)
Yes

Как можно скорее тестирование с постепенным выбором приводит к гораздо более эффективному коду. Мне нравится использовать свой собственный select/2, который позволяет мне однозначно выбирать элементы списка из домена (т. Е. Другого списка, которому разрешено быть длиннее первого, поэтому permutation/2 нельзя использовать).

select([A|As],S):- select(A,S,S1),select(As,S1).
select([],_). 

sisters(X):-
    maplist(fname, X, ['Paula', 'Abigail', 'Brenda', 'Mary', 'Tara']),
    maplist(month, X, [PM, AM, BM, MM, TM]),
    maplist(day,   X, [PD, AD, BD, MD, TD]),
    Months = [2,3,6,7,12],           %%% [1,2,3,4,5,6,7,8,9,10,11,12],
    Days = [sun,mon,wed,fri,sat],    %%% [sun,mon,tue,wed,thu,fri,sat], 

    select(3,Months,M2),  PM = 3, 
    select(PD,Days,D2),   PD \== sat,              % 1a
    select(AD,D2,D3),     AD \== fri, AD \== wed,  % 1b
    select(TM,M2,M3),     TM =\= 2,                % 3a
    select(MM,M3,M4),     MM =\= 12,  MM =\= 7,    % 4a1 % 5c
    select(TD,D3,D4),  select([TD,MD],[sat,sun]),  % 3b  % 4a2
    month(G,6), day(G,sun), member(G,X),           % 4b
    select([MD,BD],D4),   BD \== fri,              % 5a
    select([BM,AM],M4),   TM < BM,                 % 5b
    day(G2,mon),          member(G2,X),
    month(G2,G2M),        G2M < BM, G2M < MM.      % 2

Запустить его:

?- sisters(X).
X = [d('Paula', fri, 3), d('Abigail', mon, 2), d('Brenda', wed, 12), 
     d('Mary', sun, 6), d('Tara', sat, 7)] ;
No

?- time(sisters(_)).
% 2,071 inferences, 0.00 CPU in 0.00 seconds (?% CPU, Infinite Lips)
Yes

?- time( (sisters(_),fail;true) ).  % exhaust the search space
% 2,450 inferences, 0.00 CPU in 0.00 seconds (?% CPU, Infinite Lips)
Yes

Используя все 12 месяцев в году и 7 дней недели (что я сделал сначала, к сожалению :)), есть 4561 решение, которое 2-й код находит достаточно быстро (0,16 секунды, 424 600 выводов). Для первого кода с select/2 вместо permutation/2 требуется 180 400 000 выводов и 75 секунд для получения только первого ответа, по сравнению с 19 400 инф. За 0,01 секунды для второго, быстрее. код.

person Will Ness    schedule 04.09.2012
comment
Я не согласен, когда вы пишете месяц (G, 6), день (G, солнце), (G = Мэри; G = Тара),% 4b, правило 4b просто говорит: Девушка, чей день рождения был в июне, родилась в воскресенье . вы не выходите из поиска Prolog! (надеюсь, что вы меня понимаете) - person joel76; 04.09.2012

Пролог подхода #clpfd: -

:-use_module(library(clpfd)).
puzzle(Sisters,Months,Days):-
Sisters=[Paula, Brenda, Abigail, Mary, Tara], Sisters ins 1..5,
Months=[Feburary, March, June, July, December], Months ins 1..5,
Days=[Monday, Wednesday, Friday, Saturday, Sunday], Days ins 1..5,

Paula#=March,
Paula#\=Saturday,
Abigail#\=Friday #\/ Abigail #\=Wednesday,
Tara#\=Feburary #/\ (Tara#=Saturday #\/ Tara#=Sunday),
Mary#\=December #/\ (Mary#\=Saturday #\/ Mary#\=Sunday),
Tara#=Brenda-1,
Brenda#\=Friday,
Mary#\=July,
June#=Sunday,
Brenda #\=Monday #/\ Mary #\=Monday,

all_different(Sisters),
all_different(Months),
all_different(Days),

labeling([], Sisters), labeling([],Months), labeling([], Days).

?-puzzle(Sisters,Months,Days).
OUTPUT:
Days = [1, 3, 4, 2, 5],
Months = [3, 1, 5, 2, 4],
Sisters = [1, 3, 4, 5, 2]
Days = [4, 3, 1, 2, 5],
Months = [3, 1, 5, 2, 4],
Sisters = [1, 3, 4, 5, 2]
Days = [1, 3, 4, 2, 5],
Months = [3, 1, 5, 4, 2],
Sisters = [1, 3, 4, 5, 2]
......
person Reema Q Khan    schedule 13.12.2020