看看是否有可能通过Prolog的火车路线

时间:2013-02-26 22:59:03

标签: recursion prolog

我正在与Prolog合作的一个问题是看火车是否可以从一个目的地旅行到另一个目的地。有两条规则。

  1. 火车可以通过一个或多个中介从一个目的地到另一个目的地 例如:旧金山到洛杉矶     洛杉矶到欧文湾     欧文到圣地亚哥
    这提供了从旧金山到圣地亚哥的路线。

  2. 火车可以往返目的地。因此,如果火车可以从旧金山前往洛杉矶,它可以从洛杉矶前往旧金山。

  3. 这是我目前的代码。

    nonStopTrain(sandiego,oceanside).
    nonStopTrain(lasvegas,sandiego).
    nonStopTrain(sanfrancisco,bakersfield).
    nonStopTrain(bakersfield,sandiego).
    nonStopTrain(oceanside,losangeles).
    nonStopTrain(portland,sanfrancisco).
    nonStopTrain(seattle,portland).
    
    canTravel(From, To) :- nonStopTrain(From, To); nonStopTrain(To, From).
    canTravel(From, To) :-
        canTravel(From, Through), canTravel(Through, To).
    

    问题是双向旅行的能力。当我运行这个程序时,我会继续在同一个地方跑回第四,我不确定为什么。

3 个答案:

答案 0 :(得分:3)

一个天真的解决方案的问题是,如果你不消除周期,有很多方法可以从A点到达B点。假设我想从西雅图前往旧金山。如果没有处理周期,我们将把这些作为一个独特的解决方案:

seattle ->  portland -> seattle -> portland -> sanfrancisco
seattle ->  portland -> seattle -> portland -> seattle -> portland -> sanfrancisco
seattle -> (portland -> seattle) x N -> sanfrancisco

您可以自行加倍的次数没有限制,因此只要连接三个节点就可以实现无限数量的解决方案。在实践中,你不希望任何解决方案,你自己加倍,但Prolog不知道,并没有直观和天真的方法来防止它。

前进的更好方法之一就是简单地跟踪你去过的地方。要做到这一点,我们需要让谓词进行额外的论证。首先,我还介绍了辅助谓词。

connectedDirectly(From, To) :- nonStopTrain(From, To) ; nonStopTrain(To, From).

如果我们真的只想在行程中附加一条腿,那么将它分开将减少递归调用canTravel的愿望。现在是canTravel

canTravel(From, To)    :- canTravel(From, To, []).

这是一个arity 2规则,映射到我们新的arity 3规则。我们最初的地方列表总是空的。现在我们需要一个基本案例:

canTravel(From, To, _) :- connectedDirectly(From, To).

这应该是显而易见的。现在归纳的情况有点不同,因为我们需要做两件事:找到一条新的腿来附加旅程,确保我们之前没有通过那条腿,然后重复,添加新腿到我们去过的地方名单。最后,我们希望确保我们不会在我们开始的地方进行大规模的循环,因此我们在最后添加一条规则以确保我们不会。

canTravel(From, To, Visited) :-
  connectedDirectly(From, Through),
  \+ memberchk(Through, Visited),
  canTravel(Through, To, [Through|Visited]),
  From \= To.

现在,如果你运行它,你会发现你得到了98个解决方案,并且所有解决方案都是对称的:

?- forall(canTravel(X, Y), (write(X-Y), nl)).
sandiego-oceanside
lasvegas-sandiego
sanfrancisco-bakersfield
... etc.

所以,幸运的是,我们能够避免使用广度优先的搜索解决方案。

修改

我显然通过为两个单独的谓词重载名称canTravel来混淆这种情况。在Prolog中,谓词由名称​​和唯一定义,就像在C ++或Java中重载一样,"有效的方法"由参数的数量和名称决定,而不仅仅是名称。

你的直觉是正确的 - canTravel(From, To) :- canTravel(From, To, [])中的空列表正在为访问过的地点列表建立初始绑定。它并没有像建立一个基本情况那样完全分配存储空间。

canTravel本身就有两种用途。其中一个是从canTravel/3致电canTravel/2。在这种情况下,canTravel/3实际上就像一个助手,执行canTravel/2的实际工作,但是我们正在初始化为空列表的内部变量。另一个用途是来自canTravel/3内的canTravel/3,为此,我们都使用它来实现相同的目标:递归,Prolog的主要"循环"施工。

canTravel(From, To, _) :- connectedDirectly(From, To)中的第三个参数是使该子句成为canTravel/3的一部分的原因。这是递归的基本情况,因此它不需要考虑到目前为止我们访问过的地方(尽管归纳情况会阻止循环旅程)。我们也可以在这里查看它,但事实证明它更昂贵并且对结果集没有影响:

canTravel(From, To, Visited) :- connectedDirectly(From, To), \+ memberchk(To, Visited).

我的结论是,如果在不更改答案的情况下增加费用和复杂性,我们可以省略检查,这会将基本案例减少到使用匿名第三变量的原始案例。

在没有重载的情况下看到它可能更有意义,在这种情况下它看起来像这样:

canTravel(From, To) :- canTravel_loop(From, To, []).

canTravel_loop(From, To, _) :- connectedDirectly(From, To).
canTravel_loop(From, To, Visited) :-
  connectedDirectly(From, Through),
  \+ memberchk(Through, Visited),
  canTravel_loop(Through, To, [Through|Visited]),
  From \= To.

修改2

关于" bar运算符,"你的直觉再次正确。 :)我在这里使用它来将一个项目添加到列表中。令你感到困惑的是,在Prolog中,大多数表达都是统一的,表达关系而不是程序。因此,根据上下文,[X|Xs]可能用于构造新列表(如果您手头有X和XS),或者它可能用于将隐式列表分解为头X和尾{ {1}}。看看我只能从repl中使用它的所有方法:

Xs

这基本上是我们在?- X = hello, Xs = [world, new, user], Y = [X|Xs]. Y = [hello, world, new, user]. 中使用它的方式:我们已经通过了访问,因此我们正在创建一个新的列表,其中包含“开始”和“访问为尾”,以及#39;是递归调用的第三个参数。在程序方面,我们只是将“通过”添加到我们循环中使用的变量中。

但是因为这是Prolog,我们不仅限于在一个方向上使用东西:

canTravel

请注意,Prolog并不关心作业发生的方向,但是它设法向后工作"找出X和X应该使用Y.这是关于Prolog的神奇之处。 (请注意,在本次会议的示例中,我省略了回显的变量,因为它们模糊了这一点。)

通常,您需要可以求解不同参数的谓词。例如,?- Y = [hello, world, new, user], Y = [X|Xs]. X = hello, Xs = [world, new, user]. ?- Y = [hello, world, new, user], [X|Xs] = Y. X = hello, Xs = [world, new, user]. 可用于测试成员资格或枚举项目。 member/2可以从两个旧列表构建一个新列表,或者它可以枚举将列表拆分为两个段的所有方法,或者它可以找到给定列表和后缀或前缀的前缀或后缀。

随着您越来越习惯此功能,您将不再将Prolog规则视为与其他语言中的函数类似,并开始将它们视为 relationship :logical" truths&# 34;在某些结构之间存在。 append/3不是通过尝试枚举项目或通过寻找特定值的列表来编写的。它的实现是:当Item是List中的第一件事时,关系member/2 true

member(Item, List)

或者当Item在列表的其余部分时:

member(Item, [Item|_]).

此定义足以满足所有可能的用途。如果member(Item, [_|Tail]) :- member(Item, Tail). 未实例化,它将被实例化为列表中的第一个项目,然后是该列表尾部的第一个项目,依此类推。如果Item被实例化,那么如果Item是列表中的第一个项目,或者它是尾部的第一个项目,那么它将是真的。令人惊讶的是,Item甚至可用于生成包含值的列表:

member/2

你可以看到那里发生了什么:第二个子句中的?- member(1, X). X = [1|_G274] ; X = [_G8, 1|_G12] ; X = [_G8, _G11, 1|_G15] . 被制作成匿名变量,因此它生成列表,其中第一个位置为1,第二个位置为第三个,然后是第三个等等。

很多Prolog都是这样的。这个也很令人惊讶:

_

希望这有助于澄清一些事情! :)

答案 1 :(得分:0)

您是否使用过某些特定的Prolog系统?

您的程序无需修改即可正常工作(嗯,您必须在程序的第一行添加:- auto_table.)在具有表格支持的系统中,如B-Prolog。

答案 2 :(得分:0)

我认为添加剪切会阻止您的无限递归问题,因为一旦找到答案就不会永远回溯:

canTravel(From, To) :- nonStopTrain(From, To); nonStopTrain(To, From).
canTravel(From, To) :-
    canTravel(From, Through), canTravel(Through, To), !.

我毫不怀疑有一个比这更正确的解决方案。