关于构建列表直到满足条件

问题描述

我想使用 Prolog 解决 Dan finkel 的 "the giant cat army riddle"

基本上,您从 [0] 开始,然后使用以下三种操作之一构建此列表:添加 5添加 7 或取 sqrt。如果您设法建立了一个列表,使 21014 以该顺序出现在列表中,并且它们之间可以有其他数字,那么您就成功地完成了游戏。

规则还要求所有元素都是不同的,它们都是 <=60 并且都只是整数。 例如,从 [0] 开始,您可以应用 (add5,add7,add5),这将导致 [0,5,12,17],但由于它没有 2,10,14 的顺序,它不满足游戏。

我想我已经成功地编写了所需的事实,但我不知道如何实际构建列表。我认为使用 dcg一个不错的选择,但我不知道如何。

这是我的代码

:- use_module(library(lists)).
:- use_module(library(clpz)).
:- use_module(library(dcgs)).

% integer sqrt
isqrt(X,Y) :- Y #>= 0,X #= Y*Y.

% makes sure X occurs before Y and Y occurs before Z
before(X,Y,Z) --> ...,[X],...,[Y],[Z],... .
... --> [].
... --> [_],... .

% in reverse,since the operations are in reverse too.
order(Ls) :- phrase(before(14,2),Ls).

% rule for all the elements to be less than 60.
lt60_(X) :- X #=< 60.
lt60(Ls) :- maplist(lt60_,Ls).

% available operations
add5([L0|Rs],L) :- X #= L0+5,L = [X,L0|Rs].  
add7([L0|Rs],L) :- X #= L0+7,L0|Rs].
root([L0|Rs],L) :- isqrt(L0,X),L0|Rs].

% base case,the game stops when Ls satisfies all the conditions.
step(Ls) --> { all_different(Ls),order(Ls),lt60(Ls) }.

% building the list
step(Ls) --> [add5(Ls,L)],step(L).
step(Ls) --> [add7(Ls,step(L).
step(Ls) --> [root(Ls,step(L).

代码发出以下错误,但我没有试图追踪它或任何东西,因为我确信我使用的 DCG 不正确:

?- phrase(step(L),X).
caught: error(type_error(list,_65),sort/2)

我使用的是 Scryer-Prolog,但我认为 swipl 中的所有模块也都可用,例如 clpfd 而不是 clpz

解决方法

鉴于问题似乎已从使用 DCG 转移到解决难题,我想我可能会发布更有效的方法。我在 SICStus 上使用 clp(fd),但我包含了一个修改版本,该版本应该与 Scryer 上的 clpz 一起使用(用 my_simple_table/2 替换 table/2)。

:- use_module(library(clpfd)).
:- use_module(library(lists)).

move(X,Y):-
    (
      X+5#=Y
    ;
      X+7#=Y
    ;
      X#=Y*Y
    ).

move_table(Table):-
    findall([X,Y],(
            X in 0..60,Y in 0..60,move(X,Y),labeling([],[X,Y])
         ),Table).
      

% Naive version
%%post_move(X,Y):- move(X,Y).
%%
% SICSTUS clp(fd)
%%post_move(X,Y):-
%%  move_table(Table),%%  table([[X,Y]],Table).
%%
% clpz is mising table/2
post_move(X,Y):-
    move_table(Table),my_simple_table([[X,Table).

my_simple_table([[X,Table):-
      transpose(Table,[ListX,ListY]),element(N,ListX,X),ListY,Y).


post_moves([_]):-!.
post_moves([X,Y|Xs]):-
    post_move(X,post_moves([Y|Xs]).

state(N,Xs):-
    length(Xs,N),domain(Xs,60),all_different(Xs),post_moves(Xs),% ordering: 0 is first,2 comes before 10,and 14 is last.
    Xs=[0|_],element(I2,Xs,2),element(I10,10),I2#<I10,last(Xs,14).

try_solve(N,Xs):-
    state(N,Xs),labeling([ffc],Xs).
try_solve(N,Xs):-
    N1 is N+1,try_solve(N1,Xs).


solve(Xs):-
    try_solve(1,Xs).

感兴趣的两个笔记:

  • 创建一个包含可能移动的表格并使用 table/2 约束比发布约束的分离要高效得多。请注意,我们每次发布时都会重新创建表格,但我们不妨创建一次并传递它。
  • 这是使用 element/3 约束来查找和约束感兴趣的数字的位置(在本例中只有 2 和 10,因为我们可以将 14 固定在最后)。同样,这比在解决约束问题后检查顺序作为过滤更有效。

编辑:

这是一个更新版本以符合赏金约束(谓词名称,-希望- SWI 兼容,只创建一次表):

:- use_module(library(clpfd)).
:- use_module(library(lists)).

generate_move_table(Table):-
    X in 0..60,(    X+5#=Y 
    #\/  X+7#=Y 
    #\/  X#=Y*Y 
    ),findall([X,Y]),Table).
      
%post_move(X,Y,Table):- table([[X,Table). %SICStus
post_move(X,Table):- tuples_in([[X,Table). %swi-prolog
%post_move(X,Table):- my_simple_table([[X,Table). %scryer

my_simple_table([[X,Table):- % Only used as a fall back for Scryer prolog
    transpose(Table,Y).

post_moves([_],_):-!.
post_moves([X,Y|Xs],Table):-
    post_move(X,Table),post_moves([Y|Xs],Table).

puzzle_(Xs):-
    generate_move_table(Table),N in 4..61,indomain(N),length(Xs,%domain(Xs,%SICStus
    Xs ins 0..60,%swi-prolog,scryer
    
    all_different(Xs),post_moves(Xs,14 is last.
    Xs=[0|_],14).

label_puzzle(Xs):-
    labeling([ffc],Xs).

solve(Xs):-
    puzzle_(Xs),label_puzzle(Xs).

我没有安装 SWI-prolog,所以我无法测试效率要求(或者它实际上根本运行)但是在我的机器上和 SICStus 上,solve/1 谓词的新版本需要 16到 31 毫秒,而伊莎贝尔的答案 (https://stackoverflow.com/a/65513470/12100620) 中的 puzzle/1 谓词需要 78 到 94 毫秒。

至于优雅,我想这在旁观者的眼中。我喜欢这个表述,它相对清晰,展示了一些非常通用的约束(element/3table/2all_different/1),但它的一个缺点是在问题描述中序列(因此 FD 变量的数量)不是固定的,因此我们需要生成所有大小,直到匹配为止。有趣的是,似乎所有解都具有相同的长度,而且 puzzle_/1 的第一个解会生成一个长度正确的列表。

,

仅使用 dcg 构建列表的替代方法。在构建列表后检查 2,10,14 约束,因此这不是最佳的。

num(X) :- between(0,60,X).

isqrt(X,Y) :- nth_integer_root_and_remainder(2,X,0). %SWI-Prolog

% list that ends with an element.
list([0],0) --> [0].
list(YX,X) --> list(YL,[X],{ append(YL,YX),num(X),\+member(X,YL),(isqrt(Y,X); plus(Y,5,7,X)) }.
soln(X) :-
    list(X,_,_),nth0(I2,nth0(I10,nth0(I14,14),I2 < I10,I10 < I14.
?- time(soln(X)).
% 539,187,719 inferences,53.346 CPU in 53.565 seconds (100% CPU,10107452 Lips)
X = [0,12,17,22,29,36,6,11,16,4,2,9,3,15,20,25,30,35,42,49,14] 

,

我尝试了一点magic set。谓词 path/2 确实搜索了一条路径,而没有给我们一条路径。因此,我们可以使用 +5 和 +7 的交换性,减少搜索:

step1(X,Y) :- N is (60-X)//5,between(0,N,K),H is X+K*5,M is (60-H)//7,M,J),Y is H+J*7.
step2(X,0).

:- table path/2.
path(X,Y) :- step1(X,H),(Y = H; step2(H,path(J,Y)).

然后我们使用 path/2 作为 path/4 的魔法集:

step(X,Y) :- Y is X+5,Y =< 60.
step(X,Y) :- Y is X+7,0).

/* without magic set */
path0(X,L,L).
path0(X,R) :- step(X,\+ member(H,L),path0(H,[H|L],R).

/* with magic set */
path(X,L).
path(X,path(H,R).

这是一个时间比较:

SWI-Prolog (threaded,64 bits,version 8.3.16)

/* without magic set */
?- time((path0(0,[0],path0(2,H,path0(10,J,14,L))),reverse(L,R),write(R),nl.
% 13,068,776 inferences,0.832 CPU in 0.839 seconds (99% CPU,15715087 Lips)
[0,14]

/* with magic set */
?- abolish_all_tables.
true.

?- time((path(0,path(2,path(10,nl.
% 2,368,325 inferences,0.150 CPU in 0.152 seconds (99% CPU,15747365 Lips)
[0,14]

注意!

,

我设法在没有 DCG 的情况下解决了它,在我的机器上解决长度 N=24 需要大约 50 分钟。我怀疑这是因为 order 检查是从头开始对每个列表进行的。

:- use_module(library(lists)).
:- use_module(library(clpz)).
:- use_module(library(dcgs)).
:- use_module(library(time)).

%% integer sqrt
isqrt(X,Y) :- Y #>= 0,X #= Y*Y.

before(X,Z,L) :-
        %% L has a suffix [X|T],and T has a suffix of [Y|_].
        append(_,[X|T],append(_,[Y|TT],T),[Z|_],TT).

order(L) :- before(2,L).

game([X],X).
game([H|T],H) :- ((X #= H+5); (X #= H+7); (isqrt(H,X))),X #\= H,H #=< 60,X #=< 60,game(T,X). % H -> X.

searchN(N,L) :- length(L,order(L),game(L,0).