【问题标题】:Solution to Smullyan's numerical machinesSmullyan 数值机器的解决方案
【发布时间】:2014-08-10 10:11:21
【问题描述】:

在这里,我建议以defined here 为 Smullyan 的数值机找到解决方案。

问题陈述

它们是一种将数字列表作为输入的机器,并根据输入的模式遵循一些规则将其转换为另一个数字列表。 这是上面链接中给出的机器规则,表达得更正式一些。 假设 M 是机器,M(X) 是 X 的变换。 我们定义了一些这样的规则:

M(2X) = X
M(3X) = M(X)2M(X)
M(4X) = reverse(M(X)) // reverse the order of the list.
M(5X) = M(X)M(X)

并且任何不符合任何规则的东西都会被拒绝。 以下是几个例子:

  • M(245) = 45
  • M(3245) = M(245)2M(245) = 45245
  • M(43245) = 反向(M(3245)) = 反向(45245) = 54254
  • M(543245) = M(43245)M(43245) = 5425454254

问题是,找到这样的 X:

  • M(X) = 2
  • M(X) = X
  • M(X) = X2X
  • M(X) = 反向(X)
  • M(X) = 反向(X2X)反向(X2X)

这是第二个示例,它使用详尽的搜索更加复杂(尤其是如果我想要前 10 或 100 个解决方案)。

M(1X2) = X
M(3X) = M(X)M(X)
M(4X) = reverse(M(X))
M(5X) = truncate(M(X)) // remove the first element of the list truncate(1234) = 234. Only valid if M(X) has at least 2 elements.
M(6X) = 1M(X)
M(7X) = 2M(X)

问题:

  • M(X) = XX
  • M(X) = X
  • M(X) = 反向(X)

(非)解决方案

在 Prolog 中编写求解器非常简单。除了这只是详尽的探索(a.k.a brute force)并且可能需要一些时间来制定一些规则。

我试过但无法用 CLP(FD) 的逻辑约束来表达这个问题,所以我尝试用 CHR(约束处理规则)来用列表的约束(尤其是 append 约束)来表达这个问题,但是无论我如何表达,它总是归结为详尽的搜索。

问题

知道我可以采取什么方法在合理的时间内解决任何此类问题吗? 理想情况下,我希望能够生成比某个界限更短的所有解决方案。

【问题讨论】:

  • 我已经实现了简单的 Prolog 解决方案,它可以立即为您的所有问题提供答案。有哪些需要花费大量时间的问题示例?
  • 我认为 Prolog 可以很好地解决这类问题 - 它是关系型的。我看不出 CLP(FD) 如何在这里加快速度。
  • 这是关于数字还是数字列表?该链接建议数字。取 4200。链接回答 00 而不是 0
  • @SergeyDymchenko 您的 Prolog 代码是否也解决了相反的问题,例如哪个序列产生 2?
  • @hakank 是的。 length(X, _), m(X, [2]). : 'X = [2,2] ?; X = [3,2] ?;'等等。

标签: prolog constraint-programming


【解决方案1】:

让我们看看您的“更复杂一点”的问题。详尽的搜索效果很好!

这里是与Серге́й的解决方案的比较,通过考虑共同目标可以显着改进:

m([1|A], X) :-
    A = [_|_],
    append(X, [2], A).
m([E | X], Z) :-
    m(X, Y),
    (  E = 3,
       append(Y, Y, Z)
    ;  E = 4,
       reverse(Y, Z)
    ;  E = 5,
       Y = [_ | Z]
    ;  E = 6,
       Z = [1 | Y]
    ;  E = 7,
       Z = [2 | Y]
    ).

对于查询time(findall(_, (question3(X), write(X), nl), _)).,我使用 B 8.1、SICStus 4.3b8:

Серге́й B tabled   104.542s
Серге́й B          678.394s
false  B           16.013s
false  B tabled    53.007s
Серге́й SICStus    439.210s
false  SICStus      7.990s
Серге́й SWI       1383.678s, 5,363,110,835 inferences
false  SWI         44.743s,   185,136,302 inferences

其他问题并不难回答。只有具有以上m/2call_nth/2:

| ?- time(call_nth( (
        length(Xs0,N),append(Xs0,Xs0,Ys),m(Xs0,Ys),
          writeq(Ys),nl ), 10)).
[4,3,7,4,3,1,4,3,7,4,3,1,2,4,3,7,4,3,1,4,3,7,4,3,1,2]
[3,4,7,4,3,1,3,4,7,4,3,1,2,3,4,7,4,3,1,3,4,7,4,3,1,2]
[4,3,7,3,4,1,4,3,7,3,4,1,2,4,3,7,3,4,1,4,3,7,3,4,1,2]
[3,4,7,3,4,1,3,4,7,3,4,1,2,3,4,7,3,4,1,3,4,7,3,4,1,2]
[3,5,4,5,3,1,2,2,1,3,5,4,5,3,1,2,3,5,4,5,3,1,2,2,1,3,5,4,5,3,1,2]
[3,5,5,3,4,1,2,1,4,3,5,5,3,4,1,2,3,5,5,3,4,1,2,1,4,3,5,5,3,4,1,2]
[5,4,7,4,3,3,1,2,5,4,7,4,3,3,1,2,5,4,7,4,3,3,1,2,5,4,7,4,3,3,1,2]
[4,7,4,5,3,3,1,2,4,7,4,5,3,3,1,2,4,7,4,5,3,3,1,2,4,7,4,5,3,3,1,2]
[5,4,7,3,4,3,1,2,5,4,7,3,4,3,1,2,5,4,7,3,4,3,1,2,5,4,7,3,4,3,1,2]
[3,5,4,7,4,3,1,_2735,3,5,4,7,4,3,1,2,3,5,4,7,4,3,1,_2735,3,5,4,7,4,3,1,2]
196660ms

| ?- time(call_nth( (
        length(Xs0,N),m(Xs0,Xs0),
          writeq(Xs0),nl ), 10)).
[4,7,4,3,1,4,7,4,3,1,2]
[4,7,3,4,1,4,7,3,4,1,2]
[5,4,7,4,3,1,_2371,5,4,7,4,3,1,2]
[4,7,4,5,3,1,_2371,4,7,4,5,3,1,2]
[5,4,7,3,4,1,_2371,5,4,7,3,4,1,2]
[3,5,4,7,4,1,2,3,5,4,7,4,1,2]
[4,3,7,4,5,1,2,4,3,7,4,5,1,2]
[3,4,7,4,5,1,2,3,4,7,4,5,1,2]
[4,7,5,3,6,4,1,4,7,5,3,6,4,2]
[5,4,7,4,3,6,1,5,4,7,4,3,6,2]
6550ms

| ?- time(call_nth( (
        length(Xs0,N),reverse(Xs0,Ys),m(Xs0,Ys),
          writeq(Ys),nl ), 10)).
[2,1,3,4,7,1,3,4,7]
[2,1,4,3,7,1,4,3,7]
[2,1,3,5,4,7,_2633,1,3,5,4,7]
[2,1,5,4,7,3,2,1,5,4,7,3]
[2,4,6,3,5,7,1,4,6,3,5,7]
[2,6,3,5,4,7,1,6,3,5,4,7]
[2,_2633,1,5,3,4,7,_2633,1,5,3,4,7]
[2,_2633,1,5,4,3,7,_2633,1,5,4,3,7]
[2,1,3,4,4,4,7,1,3,4,4,4,7]
[2,1,3,4,5,6,7,1,3,4,5,6,7]
1500ms

【讨论】:

  • 不错的优化!在最后一个问题查询中,应该有writeq(Xs0),因为现在顺序颠倒了(并不重要,但可能会造成混淆)。
【解决方案2】:

这是对@Celelibi 改进版 (cele_n) 的另一项改进。粗略地说,它通过限制第一个参数的长度获得两倍,通过预测试两个版本获得另一个两倍。

cele_n SICStus  2.630s
cele_n SWI     12.258s 39,546,768 inferences
cele_2 SICStus  0.490s
cele_2 SWI      2.665s  9,074,970 inferences

appendh([], [], Xs, Xs).
appendh([_, _ | Ws], [X | Xs], Ys, [X | Zs]) :-
   appendh(Ws, Xs, Ys, Zs).

m([H|A], X) :-
   A = [_|_],                 % New
   m(H, X, A).

m(1, X, A) :-
   append(X, [2], A).
m(3, X, A) :-
   appendh(X, B, B, X),
   m(A, B).
m(4, X, A) :-
   reverse(X, B),
   m(A, B).
m(5, X, A) :-
   X = [_| _],
   m(A, [_|X]).
m(H1, [H2 | B], A) :-
   \+ \+ ( H2 = 1 ; H2 = 2 ),  % New
   m(A, B),
   (  H1 = 6, H2 = 1
   ;  H1 = 7, H2 = 2
   ).

answer3(X) :-
   between(1, 13, N),
   length(X, N),
   reverse(X, A),
   m(X, A).

run :-
   time(findall(X, (answer3(X), write(X), nl), _)).

【讨论】:

  • 这很有趣。如果我错了,请纠正我,但我认为第一次优化只是将搜索深度降低了一步。 \+\+ 子句是否等同于 (var(H2) ; H2 == 1 ; H2 == 2)
  • @Celelibi:是的。我也试过( var(H2) -> true ; H2 == 1 -> true ; H2 == 2 ),但\+ \+ 的效果最好。
【解决方案3】:

(我假设这是关于数字列表,正如您所建议的那样。与您提供的链接相反,它谈论数字。前导零可能存在差异。我没有花时间思考)

首先,Prolog 是一种极好的搜索蛮力的语言。因为,即使在这种情况下,Prolog 也能够减轻组合爆炸。感谢逻辑变量。

您的问题陈述本质上是存在性陈述:是否存在X 使得某某为真。这就是 Prolog 最擅长的地方。关键是你问问题的方式。与其使用[1] 等具体值,不如简单地询问:

?- length(Xs, N), m(Xs,Xs).
Xs = [3,2,3],
N = 3 ...

对于其他查询也是如此。请注意,无需满足于具体值!这肯定会使搜索成本更高!

?- length(Xs, N), maplist(between(0,9),Xs), m(Xs,Xs).
Xs = [3,2,3],
N = 3 ...

通过这种方式,可以非常有效地找到具体的解决方案,如果它们存在的话。唉,我们不能确定不存在解决方案。

为了说明这一点,这里是“最复杂”谜题的答案:

?- length(Xs0,N),
   append(Xs0,[2|Xs0],Xs1),reverse(Xs1,Xs2),append(Xs2,Xs2,Xs3), m(Xs0,Xs3).
Xs0 = [4, 5, 3, 3, 2, 4, 5, 3, 3],
N = 9,
...

它很快就会出现。但是,查询:

?- length(Xs0,N), maplist(between(0,9),Xs0),
   append(Xs0,[2|Xs0],Xs1),reverse(Xs1,Xs2),append(Xs2,Xs2,Xs3), m(Xs0,Xs3).

仍在运行!

我使用的m/2

m([2|Xs], Xs).
m([3|Xs0], Xs) :-
   m(Xs0,Xs1),
   append(Xs1,[2|Xs1], Xs).
m([4|Xs0], Xs) :-
   m(Xs0, Xs1),
   reverse(Xs1,Xs).
m([5|Xs0],Xs) :-
   m(Xs0,Xs1),
   append(Xs1,Xs1,Xs).

这更有效的原因仅仅是简单地枚举所有 n 个数字有 10n 个不同的候选者,而 Prolog 将只搜索由 3 个递归规则给出的 3n

这是另一个优化:所有 3 条规则都有相同的递归目标。那么为什么要做三次,一次就足够了:

m([2|Xs], Xs).
m([X|Xs0], Xs) :-
   m(Xs0,Xs1),
   ( X = 3,
     append(Xs1,[2|Xs1], Xs)
   ; X = 4,
     reverse(Xs1,Xs)
   ; X = 5,
     append(Xs1,Xs1,Xs)
   ).

对于最后一个查询,这从 410,014 次推理,0.094 秒 CPU 减少到 57,611 次推理,0.015 秒 CPU。

编辑:在进一步优化中,两个append/3 目标可以合并:

m([2|Xs], Xs).
m([X|Xs0], Xs) :-
   m(Xs0,Xs1),
   ( X = 4,
     reverse(Xs1,Xs)
   ; append(Xs1, Xs2, Xs),
     ( X = 3, Xs2 = [2|Xs1]
     ; X = 5, Xs2 = Xs1
     )
   ).

...这进一步将执行次数减少到 39,096 次推理,并将运行时间缩短 1 毫秒。

还有什么可以做的?长度受“输入”的长度限制。如果 n 是输入的长度,那么 2(n-1)-1 是最长的输出。这在某种程度上有帮助吗?应该不会吧。

【讨论】:

  • 由于在所有查询中我们都知道输出的长度,通过将 appendreverse 放在递归 呼叫到m/2。另外,请参阅上述问题中的新问题实例。找到前 10 个解决方案要困难得多。
【解决方案4】:

我在这里提出另一种解决方案,基本上是详尽的探索。给定问题,如果m/2 的第一个参数的长度已知,则第二个参数的长度也是已知的。如果第二个参数的长度总是已知的,则可以通过将一些约束向下传播到递归调用来减少搜索。但是,这与false提出的优化不兼容。

appendh([], [], Xs, Xs).
appendh([_, _ | Ws], [X | Xs], Ys, [X | Zs]) :-
    appendh(Ws, Xs, Ys, Zs).

m([1 | A], X) :-
    append(X, [2], A).
m([3 | A], X) :-
    appendh(X, B, B, X),
    m(A, B).
m([4 | A], X) :-
    reverse(X, B),
    m(A, B).
m([5 | A], X) :-
    B = [_, _ | _],
    B = [_ | X],
    m(A, B).
m([H1 | A], [H2 | B]) :-
    m(A, B),
    (  H1 = 6, H2 = 1
    ;  H1 = 7, H2 = 2
    ).

answer3(X) :-
    between(1, 13, N),
    length(X, N),
    reverse(X, A),
    m(X, A).

这里是分别花费的时间:这段代码,这段代码在交换递归调用与每种情况的约束时(类似于Sergey Dymchenko的解决方案),以及false的解决方案哪个因素递归调用。测试在SWI上运行,搜索长度小于等于13的所有解。

% 36,380,535 inferences, 12.281 CPU in 12.315 seconds (100% CPU, 2962336 Lips)
% 2,359,464,826 inferences, 984.253 CPU in 991.474 seconds (99% CPU, 2397214 Lips)
% 155,403,076 inferences, 47.799 CPU in 48.231 seconds (99% CPU, 3251186 Lips)

所有的措施都是通过调用来执行的:

?- time(findall(X, (answer3(X), writeln(X)), _)).

【讨论】:

  • +1:这太令人惊讶了!终止现在更糟,因为必须知道结果的长度。但是问题陈述都是这种形式!
  • 我添加了我在回答中使用的谓词。 @false,您是什么意思“终止现在更糟”?你的意思是从第一个参数计算第二个参数更难吗? (甚至不确定它会终止。)
  • (meta-remark):一旦不再需要,您可以删除您的 cmets。
  • 确切的电话会很有趣!那是时间(...)
  • @false,请注意,规则 5X 有一个附加约束,即 M(X) 必须至少有两个元素才能应用该规则。 (是的,我第一次忘记提到它,我的错。)如果您没有复制/粘贴我的代码,这可能解释了推理数量的差异。
【解决方案5】:

制表(记忆)可以帮助解决问题的更难变体。

这是我对 B-Prolog 中第二个示例的第三个问题的实现(返回长度为 13 或更短的所有解决方案):

:- table m/2.

m(A, X) :-
    append([1 | X], [2], A).
m([3 | X], Z) :-
    m(X, Y),
    append(Y, Y, Z).
m([4 | X], Z) :-
    m(X, Y),
    reverse(Y, Z).
m([5 | X], Z) :-
    m(X, Y),
    Y = [_ | Z].
m([6 | X], Z) :-
    m(X, Y),
    Z = [1 | Y].
m([7 | X], Z) :-
    m(X, Y),
    Z = [2 | Y].

question3(X) :-
    between(1, 13, N),
    length(X, N), 
    reverse(X, Z), m(X, Z).

运行:

B-Prolog Version 8.1, All rights reserved, (C) Afany Software 1994-2014.
| ?- cl(smullyan2).
cl(smullyan2).
Compiling::smullyan2.pl
compiled in 2 milliseconds
loading...

yes
| ?- time(findall(_, (question3(X), writeln(X)), _)).
time(findall(_, (question3(X), writeln(X)), _)).
[7,3,4,1,7,3,4,1,2]
[7,4,3,1,7,4,3,1,2]
[3,7,4,5,1,2,3,7,4,5,1,2]
[7,4,5,3,1,_678,7,4,5,3,1,2]
[7,4,5,3,6,1,7,4,5,3,6,2]
[7,5,3,6,4,1,7,5,3,6,4,2]
[4,4,7,3,4,1,4,4,7,3,4,1,2]
[4,4,7,4,3,1,4,4,7,4,3,1,2]
[5,6,7,3,4,1,5,6,7,3,4,1,2]
[5,6,7,4,3,1,5,6,7,4,3,1,2]
[5,7,7,3,4,1,5,7,7,3,4,1,2]
[5,7,7,4,3,1,5,7,7,4,3,1,2]
[7,3,4,4,4,1,7,3,4,4,4,1,2]
[7,3,4,5,1,_698,7,3,4,5,1,_698,2]
[7,3,4,5,6,1,7,3,4,5,6,1,2]
[7,3,4,5,7,1,7,3,4,5,7,1,2]
[7,3,5,6,4,1,7,3,5,6,4,1,2]
[7,3,5,7,4,1,7,3,5,7,4,1,2]
[7,3,6,5,4,1,7,3,6,5,4,1,2]
[7,4,3,4,4,1,7,4,3,4,4,1,2]
[7,4,3,5,1,_698,7,4,3,5,1,_698,2]
[7,4,3,5,6,1,7,4,3,5,6,1,2]
[7,4,3,5,7,1,7,4,3,5,7,1,2]
[7,4,4,3,4,1,7,4,4,3,4,1,2]
[7,4,4,4,3,1,7,4,4,4,3,1,2]
[7,4,5,6,3,1,7,4,5,6,3,1,2]
[7,4,5,7,3,1,7,4,5,7,3,1,2]
[7,5,6,3,4,1,7,5,6,3,4,1,2]
[7,5,6,4,3,1,7,5,6,4,3,1,2]
[7,5,7,3,4,1,7,5,7,3,4,1,2]
[7,5,7,4,3,1,7,5,7,4,3,1,2]
[7,6,5,3,4,1,7,6,5,3,4,1,2]
[7,6,5,4,3,1,7,6,5,4,3,1,2]

CPU time 25.392 seconds.
yes

所以这个特殊问题不到一分钟。

我认为约束编程对这类问题没有任何帮助,尤其是对于“找到 20 个第一个解决方案”变体。

更新:同一程序在我的电脑上不同系统的运行时间:

B-Prolog 8.1 with tabling: 26 sec
B-Prolog 8.1 without tabling: 128 sec
ECLiPSe 6.1 #187: 122 sec
SWI-Prolog 6.2.6: 330 sec

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-09-04
    • 2023-01-24
    • 1970-01-01
    • 2019-11-21
    • 2022-12-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多