在Prolog编程中使用Cut和Not

时间:2017-08-04 18:38:35

标签: prolog prolog-cut

我是Prolog编程的新手。所以我想知道以下内容。我知道在Prolog中使用Cut(!)。有人可以解释我,在Prolog中使用Not,何时以及如何使用Not& amp;如何在不使用Cut和避免回溯的情况下重写以下内容。 (仅使用谓词)

choose (0,_,[]) :- !.
choose (N,[H|T], [H|R]) :- M is N-1, Choose (M,T,R).
choose(N, [_|T],R) :- Choose(N,T,R)

并解释我如何重写以下内容(仅使用谓词)

chooseAll(N,L,Res) :-
    chooseAll(N,L,[],Res).

chooseAll(N,L,Seen,Res):-
    choose(N,L,R),
    not(member(R,Seen)),
    !,
    chooseAll(N,L,[R|Seen],Res).
chooseAll(_,_,Res,Res).

2 个答案:

答案 0 :(得分:3)

not/1是一个带有一个参数的谓词:目标(在您的示例中为member(R,Seen))。

它根据 否定作为有限失败原则。这意味着not(P)成功,因为Prolog找到 no 方式来满足P。换句话说,如果您直接查询Prolog P,它将返回false。如果Prolog与P进入无限循环,它也将永远循环not(P)

如果另一方面P成功(有统一与否),not(P)将会失败。

由于只有Pfalse才会成功,因此not不会进行统一。在P成功的情况下,not(P)失败,因此撤消任何统一。

因此not(member(R,Seen))会检查R Seen的成员。如果Seen是变量,not(member(R,Seen))将失败。鉴于R是变量,not(member(R,Seen))将成功,因为Seen不是空列表。

另一方面,剪切 ! not/1无关。在您的示例中,它仅表示如果not(member(R,Seen)成功,则会执行剪切,因此在该情况下将不会执行最后chooseAll/1上的重做。

那么chooseAll/4谓词是如何工作的呢?它以Seen作为空列表调用。在第一个子句中,我们首先调用choose(N,L,R),它会在列表R中生成数字组合。我们希望阻止同时生成相同的列表,因此接下来使用not(member(R,Seen))调用来检查R中是否已Seen。对于第一个结果,情况并非如此,因此我们剪切!以阻止系统回溯并选择最后一个子句,接下来我们递归调用chooseAll,但添加了R Seen列表。

在递归调用中,我们再次询问choose/3生成组合R并将其添加到显示的列表中。但现在它将生成与第一次相同的结果:choose/3将为每个调用以相同的顺序创建答案。这意味着Prolog将回溯not(member(R,Seen))并要求choose/3生成下一个组合。该组合不会成为Seen的一部分,因此not(member(R,Seen))会成功,因此我们会再次切换并将R添加到Seen进行递归调用。

这将一直持续到choose/3完全耗尽,并且未能提出尚未成为R一部分的Seen组合。在这种情况下,Prolog将回溯chooseAll/4谓词,从而执行最后一个谓词,将结果ResSeen列表统一起来。

然而,这是一种效率低下的方法:对于我们生成的每个答案,我们将再次调用谓词,直到它生成新的答案。因此,ISO谓词findall/3谓词。它会将谓词的所有结果放在一个列表中(而不是以相反的顺序)。我们可以像:

一样使用它
chooseAll(N,L,Res) :-
    findall(R,choose(N,L,R),Res).

注意:由于not/1上的documentation指定:

  

如果无法证明Goal,则为真。 仅保留兼容性。新代码应使用\+/1

所以你最好使用\+ member(R,Seen),因为它更像助记符

答案 1 :(得分:2)

请让我们简单回过头来考虑一些比如何使用一个或两个特定谓词(如!/0not/1)更为普遍的问题。

首先,让我们修复一些语法问题,并将原始代码读取:

choose(0,_,[]) :- !.
choose(N,[H|T], [H|R]) :- M is N-1, choose(M,T,R).
choose(N, [_|T],R) :- choose(N,T,R).

chooseAll(N,L,Res) :-
    chooseAll(N,L,[],Res).

chooseAll(N,L,Seen,Res):-
    choose(N,L,R),
    not(member(R,Seen)),
    !,
    chooseAll(N,L,[R|Seen],Res).
chooseAll(_,_,Res,Res).

从这开始,我应用以下两个小的改动:

  • 我使用not/1而不是(\+)/1,因为(\+)/1是ISO,但not/1是,不是
  • 而不是(is)/2,我使用CLP(FD)约束(#=)/2来宣传更新的语言结构,并明确指出我们只推理 整数 在这种情况下,不是浮动或其他类型的数字。可以将其视为保证额外的类型安全性

我们获得了这两个小的变化:

choose(0,_,[]) :- !.
choose(N,[H|T], [H|R]) :- M #= N-1, choose(M,T,R).
choose(N, [_|T],R) :- choose(N,T,R).

chooseAll(N,L,Res) :-
    chooseAll(N,L,[],Res).

chooseAll(N,L,Seen,Res):-
    choose(N,L,R),
    \+ member(R,Seen),
    !,
    chooseAll(N,L,[R|Seen],Res).
chooseAll(_,_,Res,Res).

现在让我们开始!我很想通过询问来试用主谓词:有哪些解决方案?

为了找到答案,我发布了所谓的最一般的查询,其中所有参数都是新变量:

?- chooseAll(X, Y, Z).
X = 0,
Z = [[]].

什么?这个谓词只有一个解决方案,对吧?

可能不是。

所以,我想为这些基本问题得到更多答案。为了得到它们,我做了以下额外的更改:

  • 删除!/0
  • 我使用 dif/2 表示两个字词不同
  • 我稍微重新排序了这些条款。
  • 我为那些仅在N #> 0大于0时适用的条款添加约束N

所以,我们有:

choose(0,_,[]).
choose(N,[H|T], [H|R]) :- N #> 0, M #= N-1, choose(M,T,R).
choose(N, [_|T],R) :- N #> 0, choose(N,T,R).

chooseAll(N,L,Res) :-
    chooseAll(N,L,[],Res).

chooseAll(_,_,Res,Res).
chooseAll(N,L,Seen,Res):-
    choose(N,L,R),
    maplist(dif(R), Seen),
    chooseAll(N,L,[R|Seen],Res).

现在我们有了例如:

?- chooseAll(X, Y, Z).
Z = [] ;
X = 0,
Z = [[]] ;
X = 1,
Y = [_1966|_1968],
Z = [[_1966]] ;
X = 1,
Y = [_3214, _3220|_3222],
Z = [[_3220], [_3214]],
dif(_3220, _3214) ;
X = 1,
Y = [_3560, _3566, _3572|_3574],
Z = [[_3572], [_3566], [_3560]],
dif(_3572, _3560),
dif(_3572, _3566),
dif(_3566, _3560) .
X = 0,
Z = [[]].

我将其留作练习,以确定此程序现在是否过于笼统,过于具体或两者兼而有之,并添加或删除必要的约束以仅获取 期望的答案。

我想表明的主要观点是坚持使用纯谓词可以帮助您获得更多通用程序。使用!/0(\+)/1对此没有帮助。为了避免在保留谓词的一般性的同时回溯特定情况,请使用新谓词if_/3