解决Prolog中的逻辑谜题

时间:2017-06-03 20:32:21

标签: prolog clpfd

我正处于学习prolog的“早期阶段”,并且遇到了一个易于实现接缝的逻辑谜语:

Link to the riddle | Link to solution

我们正在寻找满足以下条件的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整除,如果它的横向和可被9整除。
  • 如果最后一位数字为0,则整数可以被10整除而没有余数。

我在prolog中阅读了多个规则的介绍,但仍然没有得到它是如何做到的。有人可以帮忙吗?会很棒:))

3 个答案:

答案 0 :(得分:3)

由于您已使用对此进行了标记,因此我想使用约束的其他信息来扩充现有答案。

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

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

digits_integer(Ds0, I) :-
        reverse(Ds0, Ds),
        Ds0 ins 0..9,
        foldl(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) :-
        length(Prefix, N),
        append(Prefix, _, Ls),
        digits_integer(Prefix, I),
        I mod N #= 0.

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

solution(Ds) :-
        length(Ds, 10),
        Ds ins 0..9,
        all_distinct(Ds),
        E in 2..10,
        findall(E, indomain(E), Es),
        maplist(n_divisible(Ds), Es).

示例查询:

?- solution(Ds), label(Ds).
Ds = [3, 8, 1, 6, 5, 4, 7, 2, 9, 0] ;
false.

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

?- time((puzzle(Vs),false)).
% 142,709,119 inferences, 14.865 CPU in 14.867 seconds

VS:

?- time((solution(Ds),label(Ds),false)).
% 19,384,173 inferences, 2.166 CPU in 2.166 seconds

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

答案 1 :(得分:3)

这是与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,它是{{的第二个参数1}}。与@ 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.

答案 2 :(得分:1)

在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版本快几倍。