Prolog-无限循环



我想检查元素是否在列表中间。我搜索中间元素,接下来我检查是否是列表的成员,但是我会获得无限循环。

我的谓词:

remove_first([_,H1|T], [H1|T]).
remove_last([_],[]).
remove_last([H|T], [H|T2]) :- remove_last(T, T2).
remove_first_and_last([X],[X]).
remove_first_and_last(In, Out) :- 
    remove_first(In, Out1),
    remove_last(Out1, Out).
middle([X], [X]).
middle(In, X) :-
    remove_first_and_last(In, Out),
    middle(Out, X).
member(X, [X|_]).
member(X, [_|T]) :- member(X, T).
is_middle(X, In) :-
    middle(In, Out),
    member(X, Out), !.

,当我致电is_middle(1,[2,1,3])时,我将变得正确。但是当我致电is_middle(1,[2,2,3])时,我没有得到结果。解释器不会中断处理。

在您的情况下,您有两个选择。您可以在另一个答案中看到的痕迹沿痕迹的文本墙壁涉水,或者首先尝试减少您必须理解的内容。我更喜欢后者,因为我不喜欢读太多。

但是您的主要问题是这个。你说:

,当我致电is_middle(1,[2,1,3])时,我将获得真实。

是的,Prolog找到了一个解决方案,但它没有发现一次,而是无限多次。只需点击 space ; 看到以下内容:

?- is_middle(1,[2,1,3]).
   true
;  true
;  true
;  true
;  true
;  ... .

所以,您的第一个查询已经有问题。观察此问题的最佳方法是将 false 添加到此查询:

? -  is_middle(1,[2,1,3](, false 。   循环。

现在,让我们尝试减少查询的大小。我们可以将其缩小到:

? -  is_middle(1,[1](, false 。   循环。

这样,我们现在可以查看您的程序。在其他任何事情之前,我将删除切口。无论如何它都放错了位置。

要了解实际发生的事情,我将通过将 false 插入其中来缩小您的程序范围。有了这些额外的目标,可以消除许多不必要的细节。尽管如此,剩下的名为"失败板"的程序仍然与我们相关,如果它仍在循环。

remove_first_and_last([x],[x](。 remove_first_and_last(in,out(: -   false  remove_first(in,out1( remove_last(out1,out(中间([x],[x](: -   false 。中间(in,x(: -     remove_first_and_last(in,out(,    中间(x(, false 。is_middle(x,in(: -     中间(in,in(, false 成员(x,out(

将此与您的原始程序进行比较!读数要少得多。要解决问题,您必须在其余片段中修复一些问题。我建议删除remove_first_and_last([X],[X]).的事实,这一事实暗示了某些东西已被删除,但是对于这种情况,什么也没有删除。


用于直接使用DCG的解决方案:

is_middle(E, Es) :-
   phrase(middle(E), Es).
middle(E) --> [E].
middle(E) --> [_], middle(E), [_].

最短,但是它有一个很小的问题:它不能确定地计算答案。您可以通过查看答案来看到这一点:

?- is_middle(1, [2,1,3]).
   true
;  false.

; false表明Prolog无法确定完成计算。换句话说,剩下一些空间。您可能很想使用切口。抵抗!


如果您真的很快,请使用最快的版本:

is_middle(X, Xs) :-
   Xs = [_|Cs],
   middle_el(Cs, Xs, X).
middle_el([], [X|_], X).
middle_el([_,_|Cs], [_|Xs], X) :-
   middle_el(Cs, Xs, X).

如果您想要@daniellyons的解释,该解释承认长度列表具有两个中间元素,请查看在语法定义以上采用的容易。只需添加以下两个规则:

middle(E) --> [E,_].
middle(E) --> [_,E].

或者,将所有四个规则组合成一个:

middle(E) --> [E] | [E,_] | [_,E] | [_], middle(E), [_].

对于最快的解决方案,情况有些复杂...

is_middle_dl(X, Xs) :-
   Xs = [_|Cs],
   middle_el_dl(Cs, Xs, X).
middle_el_dl([], [X|_], X).
middle_el_dl([_|Cs], Xs, X) :-
   middle_el_dl2(Cs, Xs, X).
middle_el_dl2([], [A,B|_], X) :-
   ( X = A ; X = B ).
middle_el_dl2([_|Cs], [_|Xs], X) :-
   middle_el_dl(Cs, Xs, X).

检查它,我使用:

?- length(Xs, N), N mod 2 =:= 0, is_middle_dl(X, Xs).
   Xs = [X,_A], N = 2
;  Xs = [_A,X], N = 2
;  Xs = [_A,X,_B,_C], N = 4
;  Xs = [_A,_B,X,_C], N = 4
;  Xs = [_A,_B,X,_C,_D,_E], N = 6
;  Xs = [_A,_B,_C,X,_D,_E], N = 6
;  Xs = [_A,_B,_C,X,_D,_E,_F,_G], N = 8
;  Xs = [_A,_B,_C,_D,X,_E,_F,_G], N = 8
;  Xs = [_A,_B,_C,_D,X,_E,_F,_G,_H,_I], N = 10
;  Xs = [_A,_B,_C,_D,_E,X,_F,_G,_H,_I], N = 10
;  ... .

调试序言需要一些不同的技能,所以让我们走很长的路。

首先,让我们注意到有关您的两个示例查询的一些有趣的东西。第一个成功,应该;第二个应该失败,而是循环。这个花絮是一个线索:它表明我们正在尝试处理虚假案例。这是使用其他语言的序言的人们中的一个普遍错误。在Prolog中,通常足以明确有关成功的案件,而只是通过失败的统一发生失败。

调试Prolog的标准工具是trace/0。这个想法是,您激活跟踪模式,然后运行查询,例如:

?- trace,  is_middle(1,[2,2,3]).

trace/0的麻烦在于,它可以花一些努力来了解它正在发生的事情。每行以这四个动词之一开始:呼叫,出口,重做或失败。然后有一个指示调用的嵌套级别的数字。呼叫和重做动词告诉您您正在输入计算;出口和失败告诉您计算停止,嵌套级别即将降低。呼叫/出口是正常情况,失败/重做是使Prolog与众不同的,即非确定性的。通常,无限的循环看起来像是有意义的工作(或可能不是(的一些前缀,然后是trace中无数重复的输出片。我们在这里看到了。前缀:

Call: (8) is_middle(1, [2, 2, 3]) ? creep
Call: (9) middle([2, 2, 3], _G1194) ? creep
Call: (10) remove_first_and_last([2, 2, 3], _G1194) ? creep
Call: (11) remove_first([2, 2, 3], _G1194) ? creep
Exit: (11) remove_first([2, 2, 3], [2, 3]) ? creep
Call: (11) remove_last([2, 3], _G1197) ? creep
Call: (12) remove_last([3], _G1190) ? creep
Exit: (12) remove_last([3], []) ? creep
Exit: (11) remove_last([2, 3], [2]) ? creep
Exit: (10) remove_first_and_last([2, 2, 3], [2]) ? creep

重复块:

Call: (10) middle([2], _G1200) ? creep
Exit: (10) middle([2], [2]) ? creep
Exit: (9) middle([2, 2, 3], [2]) ? creep
Call: (9) member(1, [2]) ? creep
Call: (10) member(1, []) ? creep
Fail: (10) member(1, []) ? creep
Fail: (9) member(1, [2]) ? creep
Redo: (10) middle([2], _G1200) ? creep
Call: (11) remove_first_and_last([2], _G1200) ? creep
Exit: (11) remove_first_and_last([2], [2]) ? creep

现在,您可以看到,只要使用此查询,触发不良行为就会容易得多:

[trace]  ?- is_middle(2,[3]).
   Call: (7) is_middle(2, [3]) ? creep
   Call: (8) middle([3], _G398) ? creep
   Exit: (8) middle([3], [3]) ? creep
   Call: (8) member(2, [3]) ? creep
   Call: (9) member(2, []) ? creep
   Fail: (9) member(2, []) ? creep
   Fail: (8) member(2, [3]) ? creep
   Redo: (8) middle([3], _G398) ? creep
   Call: (9) remove_first_and_last([3], _G398) ? creep
   Exit: (9) remove_first_and_last([3], [3]) ? creep
   Call: (9) middle([3], _G401) ? creep
   Exit: (9) middle([3], [3]) ? creep
   Exit: (8) middle([3], [3]) ? creep
   Call: (8) member(2, [3]) ? creep
   Call: (9) member(2, []) ? creep
   Fail: (9) member(2, []) ? creep
   Fail: (8) member(2, [3]) ? creep
   Redo: (9) middle([3], _G401) ? creep

现在应该很明显,问题与middle/2remove_first_and_last/2member/2的相互作用有关。您对member/2的定义正是标准定义,因此可能不怪。现在,有趣的是,您有middle/2呼叫自身和remove_first_and_last/2middle/2remove_first_and_last/2都有一个相同的子句:m([X], [X])

这种事情是无限递归的出色生成器,因为middle/2在其 second 子句中所做的第一件事正是它只是试图做的,并且用自己的 first first 子句。因此,它可以发现自己在第二个子句中输入递归电话,完全相同的状态与较早的呼叫中的状态完全相同

解决方案是查看remove_first_and_last/2,并意识到您的第一个子句确实不是实际上删除了第一个和最后一个元素。删除remove_first_and_last([X], [X])子句修复了代码:

[trace]  ?- is_middle(2,[3]).
   Call: (7) is_middle(2, [3]) ? creep
   Call: (8) middle([3], _G398) ? creep
   Exit: (8) middle([3], [3]) ? creep
   Call: (8) member(2, [3]) ? creep
   Call: (9) member(2, []) ? creep
   Fail: (9) member(2, []) ? creep
   Fail: (8) member(2, [3]) ? creep
   Redo: (8) middle([3], _G398) ? creep
   Call: (9) remove_first_and_last([3], _G398) ? creep
   Call: (10) remove_first([3], _G398) ? creep
   Fail: (10) remove_first([3], _G398) ? creep
   Fail: (9) remove_first_and_last([3], _G398) ? creep
   Fail: (8) middle([3], _G398) ? creep
   Fail: (7) is_middle(2, [3]) ? creep
false.

您的两个测试现在也起作用:

?- is_middle(1,[2,1,3]).
true.
?- is_middle(1,[2,2,3]).
false.

我认为您在这里添加了基本案例,这是出于责任感。但是现实是,如果您有一个元素的列表,那么在任何情况下,它都应该与remove_first_and_last/2统一。这类似于使用Prolog明确处理错误情况,Prolog倾向于干扰机械的工作。

现在,缺少的一件事是,您要如何处理均匀的列表?无论有没有我的改变,您现在都不会拥有的东西。均匀的列表没有中间元素;那是你打算吗?我怀疑不是,因为is_middle/2中的member/2出现。

is_middle/2上的评论

您在这里所拥有的可能会像这样重组:

is_middle(X, In) :- middle(In, [X]).

使用member/2的用法不会购买任何东西,因为middle/2在其第二个参数中永远无法产生非单明一名列表。但是,如果这样做,因为您有均匀的列表,那将是有利可图的。您甚至可以通过将第三子句添加到middle/2

来使此代码使用以这种方式工作。
middle([X,Y], [X,Y]).

现在,请参见middle/2在诸如So:

之类的长度列表上工作。
?- middle([2,1,3,4], X).
X = [1, 3] ;
false.

现在,削减使您陷入困境。例如,1和3都是is_middle/2

?- is_middle(1, [2,1,3,4]).
true.
?- is_middle(3, [2,1,3,4]).
true.

不幸的是,如果您要求中间元素,您只有1

?- is_middle(X, [2,1,3,4]).
X = 1.

3怎么了?您阻止了它被切割产生。我不确定为什么削减在这里。我认为您必须尝试控制无限的递归,但这对您无济于事,因此请摆脱它。

通过随机添加剪切进行调试通常不是一个好主意。一种更好的方法是使用Ulrich Neumerkel的失败切片方法(请参阅本文或搜索标签以获取更多信息(。

DCG奖金

您可以将remove_first_and_last/2作为DCG规则:

remove_first_and_last --> remove_first, remove_last.

很酷,是吗?:)那是因为您在该规则中所做的那种输入/输出线程将完全转换为DCG规则。

更改的摘要

remove_first_and_last(In, Out) :- 
    remove_first(In, Out1),
    remove_last(Out1, Out).
middle([X], [X]).
middle([X,Y], [X,Y]).
middle(In, X) :-
    remove_first_and_last(In, Out),
    middle(Out, X).
is_middle(Item,List) :-
    append(Left,[Item|Right],List),
    length(Left,X),
    length(Right,X).

复杂的解决方案是不良解决方案,我的朋友。

?- is_middle(X,[1,2,3,4,5]).
X = 3 ;
false.

完全可逆的谓词:

?- is_middle(3,L).
L = [3] ;
L = [_G13, 3, _G19] ;
L = [_G13, _G19, 3, _G25, _G28] ;