解决Prolog中的逻辑谜语



我正处于学习prolog的"早期阶段",遇到了一个易于实现的逻辑谜语:

链接到谜语 |链接到解决方案


我们正在寻找满足以下条件的 10 位数字:

  • 从 0 到 9 的所有数字只出现一次。
  • 前 2 位数字可被 2 整除。
  • 前 3 位数字可被 3 整除。

前 10
  • 位数字可被 10 整除。

我想我首先需要对.pl文件实施规则,对吗? 解决方案中的规则包括:

  • 整数可以除以 1,没有余数。
  • 如果最后一个数字是直的,则整数可以被 2 整除,没有余数。
  • 如果整数的横向和可被 3 整除,则整数可以除以 3 而没有余数。
  • 如果最后两位数字可被 4 整除,则整数可以除以 4 而不带余数。
  • 如果最后一个数字能被 5 整除,则整数可被 5 整除。
  • 如果整数的横向总和可被 3 整除,最后一个数字可被 2 整除,则整数可被 6 整除,而没有余数。
  • 如果最后三位数字能被 8 整除,则整数可被 8 整除,而没有余数。
  • 如果整数的横向和可被 9 整除,则整数可被 9 整除,而没有余数。
  • 如果最后一个数字是 0,则整数可以被 10 整除,没有余数。

我在prolog中阅读了多个规则介绍,但仍然不知道该怎么做。谁能帮忙?会很棒:)

由于您已使用 clpfd 标记了此内容,因此我想用有关约束的其他信息来补充现有答案。

重要的是,约束允许您通过修剪 搜索空间来避免生成所有组合。

我们可以从将数字列表这些数字描述的整数相关联开始:

digits_integer(Ds0, I) :- 反向(Ds0, Ds), Ds0 ins 0..9, 折叠(pow, Ds, 0-0, I-_). pow(D, I0-P0, I-P) :- I #= I0 + D*10^P0, P #= P0 + 1。

下面是两个示例查询:

?- digits_integer([1,2,3],I)。I = 123。?- digits_integer(Ds,302)。Ds = [3, 0, 2] 。

接下来,让我们描述一下长度 为 N的列表Ls的前缀可被 N整除

n_divisible(Ls, N) :- 长度(前缀,N), append(前缀, _, Ls), digits_integer(前缀,I), 我修改 N #= 0。

因此,整个解决方案可以描述为:

解决方案(Ds) :- 长度(Ds, 10), Ds ins 0..9, all_distinct(Ds), E 在 2..10, findall(E, indomain(E), Es), 地图列表(n_divisible(Ds), Es).

示例查询:

?- 溶液(Ds),标签(Ds)。Ds = [3, 8, 1, 6, 5, 4, 7, 2, 9, 0] ;假。

让我们简要比较一下这两种解决方案的性能

?- time((puzzle(Vs),false)). % 142,709,119 个推理,14.865 CPU 在 14.867秒内

与:

?- time((solution(Ds),
label(Ds),false)). % 19,384,173 个推理,2.166 个 CPU 在 2.166 秒

因此,在这个具体案例中,基于约束的方法要快几倍。这主要是由于约束传播的强大功能,求解器会自动执行。

这是CLP(FD)的略有不同的方法。首先,让我们考虑一个谓词,它描述了列表、其前 n 个元素和这 n 个元素产生的数量之间的关系。这个版本有点相似,但不如@mat的digits_integer/2那么通用。

:- use_module(library(clpfd)).
digits_firstn_number_(_D,0,Num,Num).
digits_firstn_number_([D|Ds],X1,Num,Acc0) :-
X1 #> 0,
X0 #= X1-1,
Acc1 #= Acc0*10+D,
digits_firstn_number_(Ds,X0,Num,Acc1).

调用谓词num/1由一个描述实际关系的目标num_/2和一个标记数字列表的第二个目标label/1组成,这是num_/2的第二个参数。作为与@mat版本的细微区别,num/1将实际数字而不是数字列表作为参数:

num(Num) :-
num_(Num,Digits),     % <- actual relation
label(Digits).        % <- labeling the digits

实际的关系num_/2有所不同,因为可除性规则尽可能表示为对相应数字的约束(如您链接的解决方案中所建议的)而不是对相应数字的约束:

num_(Num,Digits) :-
Digits=[A,B,C,D,E,F,G,H,I,J],
Digits ins 0..9,
all_distinct(Digits),                     % divisibility for:
0 #= B mod 2,                             % <- first 2 digits
0 #= (A+B+C) mod 3,                       % <- first 3 digits
digits_firstn_number_([C,D],2,Num4,0),    % <- first 4 digits
0 #= Num4 mod 4,                          % <- first 4 digits
0 #= (E) mod 5,                           % <- first 5 digits
0 #= (A+B+C+D+E+F) mod 3,                 % <- first 6 digits
0 #= F mod 2,                             % <- first 6 digits
digits_firstn_number_(Digits,7,Num7,0),   % <- first 7 digits
0 #= Num7 mod 7,                          % <- first 7 digits
digits_firstn_number_([F,G,H],3,Num8,0),  % <- first 8 digits
0 #= Num8 mod 8,                          % <- first 8 digits
0 #= (A+B+C+D+E+F+G+H+I) mod 9,           % <- first 9 digits
J #= 0,                                   % <- all 10 digits
digits_firstn_number_(Digits,10,Num,0).   % <- the actual number

这种方法的缺点(除了更多的代码)是,它是针对这个特定的难题量身定制的,而@mat的版本可以更容易地扩展以搜索具有相似约束的不同数字数量的数字(前n位可被n整除)。从好的方面来说,这种方法更快(与SWI-Prolog(多线程,64位,版本6.6.4)相比):

?- time((num(Num),false)).
% 2,544,064 inferences, 0.486 CPU in 0.486 seconds (100% CPU, 5235403 Lips)
false.
?- time((solution(Ds),label(Ds),false)).
% 19,289,281 inferences, 3.323 CPU in 3.324 seconds (100% CPU, 5805472 Lips)
false.

当然,num/1会产生相同的解决方案:

?- num(Num).
Num = 3816547290 ;
false.

在Prolog中解决此类问题的基本方法是生成所有可能性,然后过滤它们。在这种情况下,我们需要一个没有重复的十位数字的列表,并且长度为 N 的每个前缀都应该能被 N 整除。

puzzle([A,B,C,D,E,F,G,H,I,J]) :-
select(A,[0,1,2,3,4,5,6,7,8,9],List1),
select(B,List1,List2), select(C,List2,List3), select(D,List3,List4),
select(E,List4,List5), select(F,List5,List6), select(G,List6,List7),
select(H,List7,List8), select(I,List8,List9), List9 = [J],
divisible([A,B],2),
divisible([A,B,C],3),
divisible([A,B,C,D],4),
divisible([A,B,C,D,E],5),
divisible([A,B,C,D,E,F],6),
divisible([A,B,C,D,E,F,G],7),
divisible([A,B,C,D,E,F,G,H],8),
divisible([A,B,C,D,E,F,G,H,I],9),
divisible([A,B,C,D,E,F,G,H,I,J],10).

甚至可除性也可以轻松实现:

divisible(Is,D) :-
combine(Is,N),
R is N rem D, R == 0.

但是我们还需要一堆技术细节来在整数、字符和原子之间进行转换。

:- use_module(library(lists)).
combine(Is,N) :-
maplist(conv,Is,As), concat_list(As,A),
atom_chars(A,Cs), number_chars(N,Cs).
conv(I,A) :-
number_chars(I,[C]), atom_chars(A,[C]).
concat_list([A1,A2|As],Atom) :-
atom_concat(A1,A2,A3),
concat_list([A3|As],Atom).
concat_list([A],A).

这将生成链接中指示的结果:

| ?- puzzle(X).
X = [3,8,1,6,5,4,7,2,9,0] ? ;
no
| ?- 

补充:如果你想让它更快,而不是像其他人一样购买一台更大的计算机,你可以交错代码的生成和测试部分:

puzzle2([A,B,C,D,E,F,G,H,I,J]) :-
select(A,[0,1,2,3,4,5,6,7,8,9],List1),
select(B,List1,List2), divisible([A,B],2),
select(C,List2,List3), divisible([A,B,C],3),
select(D,List3,List4), divisible([A,B,C,D],4),
select(E,List4,List5), divisible([A,B,C,D,E],5),
select(F,List5,List6), divisible([A,B,C,D,E,F],6),
select(G,List6,List7), divisible([A,B,C,D,E,F,G],7),
select(H,List7,List8), divisible([A,B,C,D,E,F,G,H],8),
select(I,List8,List9), divisible([A,B,C,D,E,F,G,H,I],9),
List9 = [J], divisible([A,B,C,D,E,F,G,H,I,J],10).

使用SWI Prolog,我得到以下时间:

?- time((puzzle(_),false)).
32m% 142,709,118 inferences, 76.333 CPU in 76.650 seconds (100% CPU, 1869553 Lips)
?- time((puzzle2(_),false)).
32m% 157,172 inferences, 0.142 CPU in 0.144 seconds (98% CPU, 1108945 Lips)
?- time((num(_),false)).
32m% 2,802,204 inferences, 1.008 CPU in 1.028 seconds (98% CPU, 2779208 Lips)

这似乎表明puzzle2版本比下面给出的num版本快几倍。

最新更新