简体   繁体   English

在 Prolog 中解决文本逻辑难题 - 查找生日和月份

[英]Solving a textual logic puzzle in Prolog - Find birthday and month

I'm reading the "7 Languages in 7 Days"-book, and have reached the Prolog chapter.我正在阅读“7 天中的 7 种语言”一书,并且已经到了 Prolog 章节。 As a learning exercises I'm trying to solve some textual logic puzzles.作为学习练习,我试图解决一些文本逻辑难题。 The puzzle goes as follow:谜底如下:

Five sisters all have their birthday in a different month and each on a different day of the week.五姐妹都在不同的月份过生日,而且每个人都在一周中的不同日子过生日。 Using the clues below, determine the month and day of the week each sister's birthday falls.使用下面的线索,确定每个姐妹生日的月份和日期。

  1. Paula was born in March but not on Saturday.宝拉出生于三月,但不是周六。 Abigail's birthday was not on Friday or Wednesday.阿比盖尔的生日不是星期五或星期三。
  2. The girl whose birthday is on Monday was born earlier in the year than Brenda and Mary.生日在星期一的女孩比布伦达和玛丽早于这一年出生。
  3. Tara wasn't born in February and her birthday was on the weekend. Tara 不是在二月出生的,她的生日是在周末。
  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.那个生日在六月的女孩是星期天出生的。
  5. Tara was born before Brenda, whose birthday wasn't on Friday. Tara 比 Brenda 早出生,Brenda 的生日不在周五。 Mary wasn't born in July.玛丽不是七月出生的。

My current implementation probably looks like a joke to experienced Prolog programmers.对于有经验的 Prolog 程序员来说,我当前的实现可能看起来像一个笑话。 The code is pasted below.代码粘贴在下面。

I would love some input on how to solve the question, and how to make the code both clear and dense.我希望就如何解决问题以及如何使代码既清晰又密集。

Ie: IE:

  1. How can I avoid typing out the limitations saying that the Days must be unique.我怎样才能避免输入限制,说 Days 必须是唯一的。
  2. How can I avoid typing out the limitations saying that the Months must be unique.我怎样才能避免输入限制,说月份必须是唯一的。
  3. Add the limitation about the ordering of the birthdays.添加关于生日排序的限制。
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.

Update Based on the answer from chac I was able to solve the puzzle.更新根据chac的回答,我能够解决这个难题。 Following the same recipe we (the programming language competency group at work) was able to solve a second puzzle as well.按照同样的方法,我们(工作中的编程语言能力小组)也能够解决第二个难题。 I have posted the complemete implementation, and example output as a gist on GitHub .我已经在 GitHub 上发布了完整的实现和示例输出作为要点

Using maplist/2 will considerably shorten your code.使用 maplist/2 将大大缩短您的代码。 For example:例如:

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

month/1 might be a better predicate name than is_month/1.月/1 可能是比 is_month/1 更好的谓词名称。 To state that two terms are different, use the constraint dif/2.要说明两个术语不同,请使用约束 dif/2。 Using maplist/2 and dif/2, you can describe that a list contains elements that are pairwise distinct:使用 maplist/2 和 dif/2,您可以描述列表包含成对不同的元素:

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

Example:例子:

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

solve/1 is an imperative name - you are describing solutions, so it is better to call it solution/1. solve/1 是一个命令式名称 - 您正在描述解决方案,因此最好将其称为 solution/1。

Maybe the riddle is underspecified, or your solution not complete: testing your code, I get也许谜语未详细说明,或者您的解决方案不完整:测试您的代码,我明白了

?- 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]] 

and yet more solutions.还有更多的解决方案。 So when is brenda born?那么布伦达什么时候出生?

A 'trick of the trade' for uniqueness is using select /3 predicate, or simply permutation /2.唯一性的“交易技巧”是使用select /3 谓词,或简单地排列/2。 Using this last the code becomes something like使用这最后的代码变得像

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 relation about 'before in year' can be coded like this: 'before in year' 的关系可以这样编码:

    ...
    % 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),
    ...

with the service predicate与服务谓词

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

The 'born in weekend' can be coded like “周末出生”可以这样编码

...
% 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),
...

and so on.等等。 After this rewrite I get the unique solution重写后,我得到了独特的解决方案

?- 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.

edit编辑

I've noted just now that I introduced some redundant member/2 and free variables, like member([brenda, C4, C10], S),... .我刚刚注意到我引入了一些冗余的 member/2 和自由变量,例如member([brenda, C4, C10], S),... Those C4, C10 obiouvsly can be replaced by the variables bound to Brenda as Month2, Day2, as was in original code.那些 C4、C10 obiouvsly 可以被绑定到 Brenda 的变量替换为 Month2、Day2,就像在原始代码中一样。

Here is a solution which uses brute-force search over the problem space.这是一个在问题空间上使用蛮力搜索的解决方案。 To say I am not proud of it would not go far enough.说我不为此感到自豪是远远不够的。 Surely there is a more elegant solution to this problem.当然,这个问题有一个更优雅的解决方案。

Anyway:反正:

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.

A better answer would implement the ordering constraints as part of the birthmonth/2 or birthday/2 clauses.更好的答案是将排序约束作为birthmonth/2birthday/2子句的一部分。 I haven't been able to get that to work, so far.到目前为止,我还无法让它发挥作用。

candidate/2 implements what amounts to a couple of nested for() loops, which you can't see but the WAM (Prolog's Warren Abstract Machine) goes through the machinations to iterate the values D1, D2, D3 ... etc. candidate/2实现了相当于几个嵌套for()循环的内容,您看不到这些循环,但 WAM(Prolog 的 Warren 抽象机)通过各种诡计来迭代值D1, D2, D3 ...等。

To see the possible answers, use:要查看可能的答案,请使用:

answer(M,D).

Keep pressing semicolon, or 'a' in gprolog to see all answers.在 gprolog 中继续按分号或“a”以查看所有答案。 The elements of each list correspond to the girls in alphabetical order.每个列表的元素按字母顺序对应于女孩。

Uniquely -selecting all entities upfront from a domain allows for an easy and simple, "both clear and dense" code.独特地- 从域中预先选择所有实体允许轻松简单,“既清晰又密集”的代码。 Using numerical domains makes for easy comparisons:使用数字域可以轻松进行比较:

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.

This finds just one solution, using only those months of year and days of week which are mentioned in the puzzle:这仅找到一种解决方案,仅使用拼图中提到的一年中的几个月和一周中的几天:

?- 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

Testing as soon as possible, selecting incrementally, leads to much more efficient code.尽快进行测试,增量选择,会产生更高效的代码。 I like using my own select/2 which lets me uniquely select elements of list from a domain (ie another list, which is allowed to be longer than the first one, so that permutation/2 can't be used).我喜欢使用我自己的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

Run it:运行:

?- 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

Using all 12 months of year and 7 days of week (which I did at first, unfortunately :) ), there are 4561 solutions, which the 2nd code finds quickly enough (0.16 secs, 424,600 inferences).使用一年中的所有 12 个月和一周中的 7 天(我一开始这样做,不幸的是:)),有 4561 个解决方案,第二个代码足够快地找到(0.16 秒,424,600 次推理)。 The 1st code, with select/2 used instead of permutation/2 , takes 180,400,000 inferences and 75 seconds to produce just the first answer, vs. 19,400 infs in 0.01 secs for the 2nd, faster code.第一个代码使用select/2而不是permutation/2 ,需要180,400,000 次推理和 75 秒才能产生第一个答案,而第二个更快的代码在 0.01 秒内需要 19,400 个 infs。

In this kind of problem, I like to follow the text of the puzzle ( works with SWI Prolog 6.3.0) :在这种问题中,我喜欢按照拼图的文本(适用于 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).

I get :我得到:

 ?- 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 .

A #clpfd approach prolog:- #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]
......

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM