您可以选择对任务采用更一般的观点。如果你想一想,取出列表的前 N 个元素并将它们附加到末尾可以看作是向左旋转 N 步(想象一下列表元素排列成一个圆圈)。 @Willem Van Onsem 的回答中的谓词名称rotate/3 也表明了这一观点。您实际上可以将这样的谓词定义为真正的关系,即使其在各个方向上都起作用。此外,最好避免对参数施加不必要的限制,同时保留良好的终止属性。为了反映谓词的关系性质,让我们选择一个描述性名称。由于第三个参数是第一个参数列表的N 步骤的左旋转,所以我们可以将其称为list_n_lrot/3 并像这样定义它:
:- use_module(library(lists)).
:- use_module(library(clpfd)).
list_n_lrot([],0,[]). % <- special case
list_n_lrot(L,N,LR) :-
list_list_samelength(L,LR,Len), % <- structural constraint
NMod #= N mod Len,
list_n_heads_tail(L,NMod,H,T),
append(T,H,LR).
list_list_samelength([],[],0).
list_list_samelength([_X|Xs],[_Y|Ys],N1) :-
N1 #> 0,
N0 #= N1-1,
list_list_samelength(Xs,Ys,N0).
list_n_heads_tail(L,N,H,T) :-
if_(N=0,(L=T,H=[]),
(N0#=N-1,L=[X|Xs],H=[X|Ys],list_n_heads_tail(Xs,N0,Ys,T))).
现在让我们逐步了解定义并通过示例观察它的一些效果。 list_n_lrot/3 的第一条规则仅用于处理空列表的特殊情况:
?- list_n_lrot([],N,R).
N = 0,
R = [] ;
false.
?- list_n_lrot(L,N,[]).
L = [],
N = 0 ;
false.
?- list_n_lrot(L,N,R).
L = R, R = [],
N = 0 ;
...
如果您不想在解决方案中包含这些案例,请忽略该规则。在整个谓词中,CLP(FD) 用于算术约束,因此list_n_lrot/3 的第二个参数可以是可变的,而不会导致实例化错误。目标list_list_samelength/2 是确保两个列表长度相同的结构约束。这有助于避免在只有第三个参数为ground 的情况下生成所有答案后出现无限循环(要看到这一点,请删除list_n_lrot/3 的前两个目标并将第三个目标替换为list_n_heads_tail(L,N,H,T),然后尝试查询?- list_n_lrot(L,N,[1,2,3]). )。这也是为什么最通用的查询是以公平的顺序列出解决方案的原因,即为每个列表长度产生所有可能性,而不是仅列出 0 步的轮换:
?- list_n_lrot(L,N,R).
... % <- first solutions
L = R, R = [_G2142, _G2145, _G2148], % <- length 3, rotation by 0 steps
N mod 3#=0 ;
L = [_G2502, _G2505, _G2508], % <- length 3, rotation by 1 step
R = [_G2505, _G2508, _G2502],
N mod 3#=1 ;
L = [_G2865, _G2868, _G2871], % <- length 3, rotation by 2 steps
R = [_G2871, _G2865, _G2868],
N mod 3#=2 ;
... % <- further solutions
最后,它还描述了两个列表的实际长度,用于在下一个目标中确定N对列表长度取模后的余数。考虑以下情况:如果将长度为 N 的列表旋转 N 步,您将再次到达初始列表。因此,旋转 N+1 步会产生与旋转 1 步相同的列表。从代数上讲,这个目标是利用同余模 N 将无限的整数集划分为有限数量的残差类的事实。因此,对于长度为 N 的列表,产生与 N 个残差类别相对应的 N 个旋转足以覆盖 所有 可能的旋转(请参阅上面的查询,了解 N=3)。另一方面,给定的 N > 列表长度 可以通过取其残差类的最小非负成员来轻松计算。例如,给定一个包含三个元素的列表,分别旋转 2 步或 5 步会产生相同的结果:
?- list_n_lrot([1,2,3],2,R).
R = [3, 1, 2].
?- list_n_lrot([1,2,3],5,R).
R = [3, 1, 2].
当然,您也可以将列表左旋转负数步,即向另一个方向旋转:
?- list_n_lrot([1,2,3],-1,R).
R = [3, 1, 2].
附带说明:由于这构成向右旋转,您可以通过简单地编写来轻松定义右旋转:
list_n_rrot(L,N,R) :-
list_n_lrot(L,-N,R).
?- list_n_rrot([1,2,3],1,R).
R = [3, 1, 2].
谓词list_n_heads_tail/4 与Willem 帖子中的splitAt/4 非常相似。然而,由于if_/3 的使用,如果list_n_lrot/3 的列表之一和第二个参数是地面,则谓词确定性地成功(在唯一的答案之后无需点击;,因为没有打开不必要的选择点) :
?- list_n_lrot2([a,b,c,d,e],2,R).
R = [c, d, e, a, b].
?- list_n_lrot2(L,2,[c,d,e,a,b]).
L = [a, b, c, d, e].
您可以观察到将 CLP(FD) 与最一般查询的第二种解决方案一起使用的另一个很好的效果:
?- list_n_lrot(L,N,R).
L = R, R = [],
N = 0 ;
L = R, R = [_G125], % <- here
N in inf..sup ; % <- here
...
此答案指出,对于具有一个元素的列表,任意旋转任意步数都会再次产生相同的列表。所以原则上,这个单一的一般答案总结了无数具体的答案。此外,您还可以提出以下问题:关于两步旋转的列表有哪些?
?- list_n_lrot2(L,2,R).
L = R, R = [_G19] ;
L = R, R = [_G19, _G54] ;
L = [_G19, _G54, _G22],
R = [_G22, _G19, _G54] ;
...
最后回到你问题中的例子:
?- list_n_lrot([1,2,3,4],2,R).
R = [3, 4, 1, 2].
请注意,这种在列表上定义任意旋转的更通用方法如何包含您将前 N 个元素重新定位到列表末尾的用例。