【问题标题】:Solving Tower of Hanoi declaratively (Prolog)以声明方式解决河内塔(Prolog)
【发布时间】:2016-05-28 13:17:18
【问题描述】:

我的教授给了this作为Prolog的一个例子。这是一个解决河内塔谜题的程序,您必须通过一个接一个地移动一个圆盘来将一堆圆盘移动到另一个钉子上,而不是将更大的圆盘放在较小的圆盘上。

现在,我不喜欢那个程序。有人告诉我 Prolog 是为声明式编程设计的。我不想编写如何解决问题的程序,我想用 Prolog 写下问题是什么。那就让Prolog来解决吧。

到目前为止,我的努力可以在下面找到。我使用了两种类型的列表,动作序列表示如下:[[1,2],[3,1]];这将是“将顶部磁盘从 peg 1 移动到 peg 2,将磁盘从 peg 3 移动到 peg 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 是完全确定的,应该只有一个结果。如何加快这个瓶颈?

【问题讨论】:

  • 我认为您的整个实施本质上是必不可少的。您是否在“河内序言塔”上进行了 stackoverflow.com 或谷歌搜索?有很多例子可以说明如何以更相关的方式做到这一点。
  • 你能解释一下它的必要性吗?对于你的问题:是的,我有,但我能找到的只是我所链接的解决方案的风格,这是公然必要的。
  • 我想我正在键入所有nth1/3 电话,正如您在问题中所指出的那样。有一些小问题需要修复,例如 [FromIdx-ToIdx | T] 之类的形式可能比 [[FromIdx | ToIdx] | T] 更合适。总的来说,您可以采用“标准”方法并使用 CLP(FD) (N #> 1, N #= N-1) 并为移动携带一个列表参数,而不是您在其他实现中通常看到的 writes。
  • 我做了一个快速而肮脏的转换,它几乎可以工作,除了像hanoi(N, [...])这样的查询,它会为一组有效的河内移动产生N,但在找到解决方案之后它确实做到了有一些……呃……终止问题。 :p

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


【解决方案1】:

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

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 看到这一点,他们会马上给出一个好的答案。)

【讨论】:

  • 你真的认为在学习 prolog 第三周的人会欣赏你的回答吗?
  • @PatrickJ.S.简短的回答是“是”。 (OP 说他们在第 3 周在哪里?)只需阅读几分钟即可了解 Prolog 中 CLP(FD) 和 DCG 的基本概念是什么,这使得它变得容易更清楚地表达这样的问题。我假设当有人问诸如如何以关系方式解决河内难题之类的问题时,他们想学习促进这种解决方案的 Prolog 方面。
  • 很好的解决方案!我的意思是实际的 Prolog 解决方案,在编辑之前! (我不认为涉及副作用的解决方案是“典型的 Prolog 解决方案”;也许是“典型的 Prolog 保龄球化”,但仅此而已。)我也将使用 DCG 和 CLP(FD) 解决方案仅 i>,也许可以稍微改进一下命名(disks_moves/2?),让它更通用一点:磁盘数量绝对适用的上限肯定是移动的长度,不是吗?这解决了终止问题。很好的关系解决方案(在编辑之前),+1!
  • @mat 谢谢。我决定将前两个添加为“对比度”,因为第一个特别出现在“网络”周围。带有附加的解决方案无疑是 icky。
  • 确实,我希望有一天“人们通常会发现的东西”“最好的东西”。我的意思是一般优雅 解决方案,我认为这对于一个好的 Prolog 程序来说更为典型。
【解决方案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.

由于我们在 List of Lists 上运行,我们需要两个 switch_nth1 调用:一个用于替换我们从中取出的 Tower,另一个用于将其放在新的 Tower 上。

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

【讨论】:

    【解决方案3】:

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

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

    让计算机在这里导出有效的解决方案对我来说似乎是一个非常困难的问题。不过对我们人类来说,只要稍微思考一下,解决方案就很明显,也去掉了所有冗余,完全没有必要进行任何比较和检查位置的合法性——解决方案是有效的,每一步都是合法的by建设

    如果我们可以移动 N = M + K 个圆盘,我们就可以移动 M 个圆盘 - 其他两个钉子是空的,我们假设较低的K 个磁盘不存在。

    但是在移动了 M 个磁盘之后,我们面临着剩下的 K。无论 M 磁盘到哪里,我们都无法将任何 K 移动到那里,因为 通过构造 K磁盘都比任何 M 都“大”(“大”只是因为它们最初在 source 挂钩上位于它们下方)。

    但是第三个挂钩是空的。将一个磁盘移到那里很容易。如果 K 等于 1,那不是很漂亮吗?将剩余的 K = 1 磁盘移动到空的 target 挂钩后,我们再次可以假装它不存在(因为它是“最大的") 并在其上移动 M 个圆盘。

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

    这一切都意味着如果我们知道如何移动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,from(source),to(target)) ;
    false.

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-27
      • 1970-01-01
      • 2016-05-25
      • 2013-08-31
      相关资源
      最近更新 更多