以声明方式解决河内塔(Prolog)

时间:2016-05-28 13:17:18

标签: prolog dcg clpfd declarative towers-of-hanoi

我的教授给了this作为Prolog的一个例子。这是一个解决河内之塔拼图的程序,你必须将一堆磁盘移动到另一个挂钩,方法是将一个磁盘移动到另一个磁盘上,而不是在较小的磁盘上放置一个更大的磁盘。

现在,我不喜欢那个节目。我被告知Prolog是用于声明性编程的。我不想编程如何解决问题,我想用Prolog写下问题。然后让Prolog解决它。

到目前为止我的努力可以在下面找到。我使用了两种类型的列表,操作序列表示如下:[[1,2],[3,1]];这将是“将顶部磁盘从挂钉1移动到挂钉2,将磁盘从挂钉3移动到挂钉1”。我的第二种类型的列表是状态,例如,如果有三个挂钩[[1,2,3], [], []]则意味着第一个挂钩上有三个磁盘。较小的磁盘具有较小的数字,因此内部列表的前面是堆栈的顶部。

% A sequence of actions (first argument) is a solution if it leads
% from the begin state (second argument) to the End state (third argument).

solution([], X, X).

solution([[FromIdx | ToIdx] | T], Begin, End) :-
    moved(FromIdx, ToIdx, Begin, X),
    solution(T, X, End).

% moved is true when Result is the resulting state after moving
% a disk from FromIdx to ToIdx starting at state Start

moved(FromIdx, ToIdx, Start, Result) :- 
    allowedMove(FromIdx, ToIdx, Start),
    nth1(FromIdx, Start, [Disk|OtherDisks]),
    nth1(ToIdx, Start, ToStack),
    nth1(FromIdx, Result, OtherDisks),
    nth1(ToIdx, Result, [Disk|ToStack]).

allowedMove(FromIdx, ToIdx, State) :- 
    number(FromIdx), number(ToIdx),
    nth1(FromIdx, State, [FromDisk|_]),
    nth1(ToIdx, State, [ToDisk|_]),
    ToDisk > FromDisk.

allowedMove(_, ToIdx, State) :- nth1(ToIdx, State, []).

上述程序似乎有效,但对于一切相当复杂的事情而言,它太慢了。要求它解决经典的河内塔问题,将三个磁盘从第一个挂钩移动到第三个和最后一个挂起,就像这样:

?- solution(Seq, [[1,2,3], [], []], [[], [], [1,2,3]]).

我想对程序进行一些修改,以便它适用于此查询。我该怎么做呢?在分析时,我可以看到nth1使用了很多时间,我应该摆脱它吗?困扰我的是moved完全是确定性的,只应该有一个结果。我怎样才能加快这个瓶颈?

3 个答案:

答案 0 :(得分:3)

对河内的Prolog解决方案通常看起来像这样。解决方案会在遇到屏幕时将移动写入屏幕,并且不会在列表中收集移动:

move_one(P1, P2) :-
    format("Move disk from ~k to ~k", [P1, P2]), nl.

move(1, P1, P2, _) :-
    move_one(P1, P2).
move(N, P1, P2, P3) :-
    N > 1,
    N1 is N - 1,
    move(N1, P1, P3, P2),
    move(1, P1, P2, P3),
    move(N1, P3, P2, P1).

hanoi(N) :-
    move(N, left, center, right).

可以修改此选项以在列表中收集移动,而不是通过添加列表参数并使用append/3

move(0, _, _, _, []).
move(N, P1, P2, P3, Moves) :-
    N > 0,
    N1 is N - 1,
    move(N1, P1, P3, P2, M1),
    append(M1, [P1-to-P2], M2),
    move(N1, P3, P2, P1, M3),
    append(M2, M3, Moves).

hanoi(N, Moves) :-
    move(N, left, center, right, Moves).

我们能够在不使用write的情况下简化基本情况。 append/3完成了这项工作,但它有点笨拙。此外,is/2特别使其成为非关系型。

通过使用DCG和CLP(FD),可以消除append/3,并且可以使其更具关系性。这就是我所说的最初“天真”的方法,它也更具可读性:

hanoi_dcg(N, Moves) :-
    N in 0..1000,
    phrase(move(N, left, center, right), Moves).

move(0, _, _, _) --> [].
move(N, P1, P2, P3) -->
    { N #> 0, N #= N1 + 1 },
    move(N1, P1, P3, P2),
    [P1-to-P2],
    move(N1, P3, P2, P1).

这导致:

| ?- hanoi_dcg(3, Moves).

Moves = [left-to-center,left-to-right,center-to-right,left-to-center,right-to-left,right-to-center,left-to-center] ? a

no
| ?- hanoi_dcg(N,  [left-to-center,left-to-right,center-to-right,left-to-center,right-to-left,right-to-center,left-to-center]).

N = 3 ? ;

(205 ms) no
| ?-

虽然它是关系型的,但确实存在一些问题:

  • “两个方向”中无用的选择点
  • 终止问题,除非受到N in 0..1000
  • 等限制

我觉得围绕这两个问题有一种方法,但还没有解决这个问题。 (我敢肯定,如果有一些比我更聪明的Prologers,比如@mat,@ false或@repeat看到这个,他们会立即得到一个很好的答案。)

答案 1 :(得分:2)

我看了你的解决方案,我想到了这个问题:

当你move时,你正在做的是从一个塔上拿下另一个塔。 有一个SWI-Predicate替换列表中的元素select/4。但是你也希望将索引替换为它。所以我们稍微重写一下,并将其称为switch_nth1,因为它不再需要对select做太多。

% switch_nth1(Element, FromList, Replacement, ToList, Index1)
switch_nth1(Elem, [Elem|L], Repl, [Repl|L], 1).
switch_nth1(Elem, [A|B], D, [A|E], M) :-
    switch_nth1(Elem, B, D, E, N),
    M is N+1.

由于我们在列表列表中运行,我们需要两个switch_nth1次呼叫:一个用于替换我们从中取出的塔,另一个用于将其放在新塔上。

move谓词看起来像这样(对不起,我稍微更改了一下参数)。 (它应该被称为allowed_move,因为它不会执行不允许的移动。)

move((FromX - ToX), BeginState, NewState):-
    % take a disk from one tower
    switch_nth1([Disk| FromTowerRest], BeginState, FromTowerRest, DiskMissing, FromX),
    % put the disk on another tower.
    switch_nth1(ToTower, DiskMissing, [Disk|ToTower], NewState, ToX),

    %  there are two ways how the ToTower can look like:
    (ToTower = [];              % it's empty
     ToTower = [DiskBelow | _], % it already has some elements on it.
     DiskBelow > Disk).

如果你把它插入你的solution,你会遗憾地遇到一些终止问题,因为没有人说已经到达的状态不应该是正确的步骤。因此,我们需要跟踪我们已经存在的位置,并在达到已知状态时禁止继续。

solution(A,B,C):-solution_(A,B,C,[B]).

solution_([], X, X,_).
solution_([Move | R], BeginState, EndState, KnownStates):-
   move(Move, BeginState, IntermediateState),
   \+ memberchk(IntermediateState, KnownStates), % don't go further, we've been here.
   solution_(R, IntermediateState, EndState, [IntermediateState | KnownStates]).

尽管如此,这个解决方案仍然非常迫切 - 应该有更好的解决方案,你真正利用recursion

答案 2 :(得分:2)

通过“声明”,我会假设你的意思是接近Prolog中“的旧口号,写下一个问题就是得到答案”。让Prolog发现答案而不是我只是在Prolog中编写我必须自己找到的答案。

简单地定义一个legal_move谓词,说明初始和最终条件并运行任何种类的标准搜索,会导致非常低效的解决方案将会回溯一大堆

让计算机在这里获得有效的解决方案对我来说似乎是一个非常难的问题。对我们这些人而言,只需要一点点思考,解决方案是显而易见的,切断所有冗余,进行任何比较并完全不必要检查位置的合法性 - 解决方案是有效的,并且每一步都是合法的 by构造

如果我们可以移动 N = M + K 磁盘,我们可以移动它们的 M - 其他两个挂钩是空的,我们假装更低 K 磁盘不在那里。

但是,移动 M 磁盘后,我们面临剩下的 K 。无论 M 磁盘到哪里,我们都无法移动任何 K ,因为按构造 K 磁盘都比任何 M “更大”(“更大”只是因为它们最初位于 peg下面)。

但第三个挂钩是空的。在那里移动一个磁盘很容易。如果 K 等于1,那不就是桃子吗?将剩余的 K = 1 磁盘移动到空的 目标 peg 后,我们再次假装它不是那里(因为它是“最大的”)并在其上移动 M 磁盘。

重要的补充:由于 M 磁盘将在第二阶段移动到目标,最初它们将被移动到 备用磁盘中

这意味着如果我们知道如何移动 M 磁盘,我们可以轻松移动 M + 1 归纳递归完成

如果你已经知道这一切,请为措词加载道歉。代码:

hanoi(Disks, Moves):- 
    phrase( hanoi(Disks, [source,target,spare]), Moves).

hanoi( Disks, [S,T,R]) -->
    { append( M, [One], Disks) }, 
    hanoi( M, [S,R,T]),
    [ moving( One, from(S), to(T)) ],
    hanoi( M, [R,T,S]).
hanoi( [], _) --> [ ].

测试:

  

4? - hanoi([1,2,3],_ X),maplist(writeln,_X)。
  移动(1,从(源)到(目标))   移动(2,从(源)到(备用))   移动(1,从(目标)到(备用))   移动(3,从(源)到(目标))   移动(1,从(备用)到(源))   移动(2,从(备用)到(目标))   移动(1,从(源)到(目标));