Recursion 通过Prolog查看是否有可能的列车路线

Recursion 通过Prolog查看是否有可能的列车路线,recursion,prolog,Recursion,Prolog,我正在用Prolog解决的一个问题是,看一列火车能否从一个目的地开到下一个目的地。有两条规则 一列火车可以通过一个或多个中间站从一个目的地开往下一个目的地。 旧金山到洛杉矶 洛杉矶到欧文 欧文到圣地亚哥 这是从旧金山到圣地亚哥的一条路线。 火车可以往返于目的地。因此,如果一列火车可以从旧金山到洛杉矶,它可以从洛杉矶到旧金山。 这是我目前拥有的代码 nonStopTrain(sandiego,oceanside). nonStopTrain(lasvegas,sandiego). nonStopT

我正在用Prolog解决的一个问题是,看一列火车能否从一个目的地开到下一个目的地。有两条规则

  • 一列火车可以通过一个或多个中间站从一个目的地开往下一个目的地。
    旧金山到洛杉矶 洛杉矶到欧文
    欧文到圣地亚哥
    这是从旧金山到圣地亚哥的一条路线。

  • 火车可以往返于目的地。因此,如果一列火车可以从旧金山到洛杉矶,它可以从洛杉矶到旧金山。 这是我目前拥有的代码

    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).
    

    问题在于双向旅行的能力。当我运行这个程序时,我总是在同一个地方来回运行,我不知道为什么。

    你使用过一些特定的Prolog系统吗


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

    我认为添加一个cut将停止无限递归问题,因为一旦找到答案,它将不会永远回溯:

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

    不过,我毫不怀疑还有比这更正确的解决方案。

    简单解决方案的问题是,如果不消除循环,从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, []).
    
    这是一个arity2规则,映射到我们新的arity3规则上。我们去过的地方一开始总是空的。现在我们需要一个基本情况:

    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/2
    调用
    canTravel/3
    。在本例中,
    canTravel/3
    实际上有点像助手,执行
    canTravel/2
    的实际工作,但是使用一个内部变量,我们正在初始化为空列表。另一个用途是
    canTravel/3
    中的
    canTravel/3
    ,因此我们都使用它来实现相同的目标:递归,Prolog的主要“循环”构造

    canTravel(From,To,uu)中的第三个参数-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

    关于“条形运算符”,您的直觉再次正确我在这里使用它将一个项目预先添加到列表中。让您困惑的是,在Prolog中,使用统一,大多数表达式表示关系而不是过程。因此,根据上下文的不同,
    [X | Xs]
    可以用来构造一个新的列表(如果您手头有X和X),也可以用来将一个隐式列表分解为头
    X
    和尾
    Xs
    。从repl查看我可以使用它的所有方法:

    ?- X = hello, Xs = [world, new, user], Y = [X|Xs].
    Y = [hello, world, new, user].
    
    这就是我们在
    canTravel
    中使用它的基本方式:我们已经通过了,我们已经访问了,所以我们制作了一个新的列表,以通过第一个和访问的作为尾部,这是递归调用的第三个参数。在程序方面,我们只是在循环中使用的变量中添加

    但因为这是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].
    
    请注意,Prolog不关心任务发生在哪个方向,但它设法“向后工作”
    member(Item, [Item|_]).
    
    member(Item, [_|Tail]) :- member(Item, Tail).
    
    ?- member(1, X).
    X = [1|_G274] ;
    X = [_G8, 1|_G12] ;
    X = [_G8, _G11, 1|_G15] .
    
    ?- length(X, 3).
    X = [_G273, _G276, _G279].