我是Prolog编程的新手。所以我想知道以下内容。我知道在Prolog中使用Cut(!)。有人可以解释一下我,在Prolog中使用Not ,何时以及如何使用Not 以及如何重写以下内容而不使用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).
not/1
是一个具有一个参数的谓词:目标(在您的示例中member(R,Seen)
)。
它根据否定作为有限失效原理工作。这意味着not(P)
成功了,因为Prolog找不到满足P
的方法。换句话说,如果您直接查询Prolog以获取P
,它将返回false
。然而,如果Prolog与P
进入无限循环,它也将在not(P)
中永远循环。
另一方面,如果P
成功(统一与否),not(P)
将失败。
由于它只有在false
P
时才成功,因此在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
完全筋疲力尽,无法提出尚未成为Seen
一部分的组合R
。在这种情况下,Prolog 将回溯chooseAll/4
谓词,从而执行最后一个谓词,这将结果Res
与Seen
列表统一起来。
然而,这是一种低效的方法:对于我们生成的每个答案,我们将再次调用谓词,直到它生成新的答案。因此,ISO 谓词是findall/3
谓词。它将谓词的所有结果放在一个列表中(而不是以相反的顺序)。我们可以像这样使用它:
chooseAll(N,L,Res) :-
findall(R,choose(N,L,R),Res).
注意:正如not/1
上的文档所指定的那样:
如果无法证明
Goal
则为真。保留仅用于兼容性。新代码应使用+/1
.
所以你最好使用+ member(R,Seen)
因为它更助记符。
请让我们退后一步,考虑一些比如何使用一个或两个特定谓词(如!/0
或not/1
)更通用的问题。
首先,让我们修复一些语法问题,并让您的原始代码读取:
选择(0,_,[]) :- !. 选择(N,[H|T], [H|R]) :- M 是 N-1,选择(M,T,R)。 选择(N, [_|T],R) :- 选择(N,T,R)。 选择全部(N,L,Res) :- 选择全部(N,L,[],Res)。 选择全部(N,L,Seen,Res):- 选择(N,L,R), not(member(R,Seen)), !, 选择全部(N,L,[R|看到],Res)。 选择全部(_,_,Res,Res)。
从此开始,我应用以下两个小更改:
- 我用
(+)/1
而不是not/1
,因为(+)/1
是ISOnot/1
但不是。 - 我不是
(is)/2
,而是使用 CLP(FD) 约束(#=)/2
来宣传更新的语言结构,并明确表示在这种情况下我们只对整数进行推理,而不是例如浮点数或其他类型的数字。将此视为保证额外的类型 安全性。
我们得到这两个小的变化:
选择(0,_,[]) :- !. 选择(N,[H|T], [H|R]) :- M #= N-1, choose(M,T,R). 选择(N, [_|T],R) :- 选择(N,T,R)。 选择全部(N,L,Res) :- 选择全部(N,L,[],Res)。 选择全部(N,L,Seen,Res):- 选择(N,L,R), \+ 成员(R,Seen), !, 选择全部(N,L,[R|看到],Res)。 选择全部(_,_,Res,Res)。
现在让我们开始吧!我渴望通过问来尝试主要谓词:有哪些解决方案?
为了找出答案,我发布了所谓的最 一般查询,其中所有参数都是新变量:
?- 选择全部(X, Y, Z).X = 0, Z = [[]]。
什么?这个谓词只有一个解决方案,对吧?右?
应该不会。
所以,我想得到这些基本问题的更多答案。为了获得它们,我进行了以下其他更改:
- 我删除
!/0
. - 我用
dif/2
来说明两个术语是不同的。 - 我稍微重新排序了子句。
- 我为那些仅在
N
大于 0 时才适用的子句添加约束N #> 0
。
因此,我们有:
选择(0,_,[])。 选择(N,[H|T], [H|R]) :- N #> 0, M #= N-1, choose(M,T,R). 选择(N, [_|T],R) :- N #> 0, choose(N,T,R). 选择全部(N,L,Res) :- 选择全部(N,L,[],Res)。 选择全部(_,_,Res,Res)。 选择全部(N,L,Seen,Res):- 选择(N,L,R), maplist(dif(R), Seen), 选择全部(N,L,[R|看到],Res)。
现在我们有例如:
?- 选择全部(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), 迪夫(_3566, _3560) . X = 0, Z = [[]]。
我把它留作一个练习,以确定这个程序现在是否太笼统,太具体或两者兼而有之,并添加或删除必要的约束以仅获得所需的答案。
我想展示的要点是,坚持纯谓词可以帮助你获得更通用的程序。使用!/0
和(+)/1
对此无济于 事。为了避免在特定情况下回溯,同时保留谓词的通用性,例如使用新的谓词 if_/3
。