不终止的原因
首先,让我们尝试理解为什么您的定义是不可逆的。我将使用failure-slice 来更好地解释它。所以考虑查询tree_len(X,1). 乍一看,一切都很完美,你甚至会得到一个很好的答案!
?- tree_len(T,1).
T = n(_G267, leaf, leaf)
但永远不要要求另一个答案,因为它会循环:
?- tree_len(T,1).
T = n(_G267, leaf, leaf) ;
** LOOPS **
所以我们得到的答案有点让人分心。乍一看似乎一切正常,但只有在回溯时才遇到真正的问题。这是在 Prolog 中习惯的东西。显然,您使用 3 的查询得到了更好的选择。但那是运气。
一般来说,有一种简单的方法可以确保这一点。只需将额外的目标 false 添加到查询中。添加false 意味着我们不再对任何答案感兴趣,因为它不再能够成功。这样一来,所有的干扰都消除了,我们直接面对问题:
?- tree_len(T,1), false.
** LOOPS **
那么,这个循环从何而来?
在纯粹的、单调的 Prolog 程序(例如这个)中,我们可以通过在我们的程序中添加一些目标 false 来定位不终止的原因。如果生成的程序(称为failure-slice)没有终止,那么原始程序也不会终止。这是我们查询的最小故障片:
?- tree_len(T,1),
假。
tree_len(n(_,leaf,leaf),1) :- false。
tree_len(n(op,G,D),N) :-
tree_len(G,TG),
假,
tree_len(D,TD),
N 是 TG+TD。
我们的程序所剩无几!正是这个微小的片段负责不终止。如果我们想解决这个问题,我们必须在那个微小的部分做点什么。其他一切都是徒劳的。
所以我们需要做的是以某种方式改变程序,使这个片段不再循环。
实际上,我们有两个选择,我们可以使用successor arithmetics 或constraints like clpfd。前者在任何 Prolog 系统中都可用,后者仅在 SWI、YAP、SICStus、GNU、B 等系统中提供。
现在,3 由s(s(s(0))) 表示。
tree_lensx(T, s(N)) :-
树_lendiff(T,N,0)。
tree_lendiff(n(_,leaf,leaf), N,N)。
tree_lendiff(n(op,G,D), s(N0),N) :-
tree_lendiff(G, N0,N1),
tree_lendiff(D, N1,N)。
我在这里使用了几种常见的编码技术。
区别
实际的关系是tree_lendiff/3,它不是用一个参数表示一个自然数,而是用两个。实际数字是两者之间的差异。通过这种方式,可以保留可逆的定义。
避免左递归
另一种技术是避免左递归。 tree_lendiff/3 描述的长度实际上是长度减一。还记得我们首先得到的故障片吗?同样的故障片也会出现在这里!然而,通过将长度“移动”一,递归规则的头部现在可以确保终止。
最初,开发有限域上的约束是为了解决组合问题。但是您也可以使用它们来获得可逆算术。 SWI 和 YAP 中的实现甚至可以编译成代码,其效率通常与传统的不可逆 (is)/2 相当,同时仍然是可逆的。
:- use_module(library(clpfd)).
tree_fdlen(n(_,leaf,leaf),1).
tree_fdlen(n(op,G,D),N) :-
N #= TG+TD,
TG #>= 1,
TD #>= 1,
tree_fdlen(G,TG),
tree_fdlen(D,TD).
此程序更符合您的原始定义。尽管如此,请注意TG #>= 1 和TD #>= 1 这两个目标添加了冗余信息以确保终止该程序。
我们现在可以像这样枚举一定范围内的所有树:
?- Size in 0..4, tree_fdlen(T, Size).
Size = 1, T = n(_A,leaf,leaf) ;
Size = 2, T = n(op,n(_A,leaf,leaf),n(_B,leaf,leaf)) ;
Size = 3, T = n(op,n(_A,leaf,leaf),n(op,n(_B,leaf,leaf),n(_C,leaf,leaf))) ;
Size = 4, ... ;
Size = 4, ... ;
Size = 3, ... ;
Size = 4, ... ;
Size = 4, ... ;
Size = 4, ... ;
false.
注意答案替换的确切顺序!它不仅仅是 1,2,3,4!而是以一些的顺序找到答案。哪一种都无所谓,只要我们对找到所有解决方案感兴趣!