针对数字板难题的优化 CLP(FD) 求解器

2024-04-10

考虑问题从https://puzzling.stackexchange.com/questions/20238/explore-the-square-with-100-hops https://puzzling.stackexchange.com/questions/20238/explore-the-square-with-100-hops:

给定一个 10x10 方格的网格,您的任务是访问每个方格一次。在每一步中,您都可以

  • 水平或垂直跳过 2 个方格或
  • 对角跳过 1 个方格

换句话说(更接近我下面的实现),用 1 到 100 之间的数字标记 10x10 网格,使得坐标处的每个方块(X, Y)是 1 或者等于 1 处的“前一个”方格(X, Y-3), (X, Y+3), (X-3, Y), (X+3, Y), (X-2, Y-2), (X-2, Y+2), (X+2, Y-2), or (X+2, Y+2).

这看起来像是一个简单的约束编程问题,Z3 可以通过简单的声明性规范在 30 秒内解决它:https://twitter.com/johnregehr/status/1070674916603822081 https://twitter.com/johnregehr/status/1070674916603822081

我在 SWI-Prolog 中使用 CLP(FD) 的实现并不能很好地扩展。事实上,它甚至无法解决问题的 5x5 实例,除非预先指定了几乎两行:

?- number_puzzle_(_Square, Vars), Vars = [1,24,14,2,25, 16,21,5,8,20 |_], time(once(labeling([], Vars))).
% 10,063,059 inferences, 1.420 CPU in 1.420 seconds (100% CPU, 7087044 Lips)
_Square = square(row(1, 24, 14, 2, 25), row(16, 21, 5, 8, 20), row(13, 10, 18, 23, 11), row(4, 7, 15, 3, 6), row(17, 22, 12, 9, 19)),
Vars = [1, 24, 14, 2, 25, 16, 21, 5, 8|...].

?- number_puzzle_(_Square, Vars), Vars = [1,24,14,2,25, 16,21,5,8,_ |_], time(once(labeling([], Vars))).
% 170,179,147 inferences, 24.152 CPU in 24.153 seconds (100% CPU, 7046177 Lips)
_Square = square(row(1, 24, 14, 2, 25), row(16, 21, 5, 8, 20), row(13, 10, 18, 23, 11), row(4, 7, 15, 3, 6), row(17, 22, 12, 9, 19)),
Vars = [1, 24, 14, 2, 25, 16, 21, 5, 8|...].

?- number_puzzle_(_Square, Vars), Vars = [1,24,14,2,25, 16,21,5,_,_ |_], time(once(labeling([], Vars))).
% 385,799,962 inferences, 54.939 CPU in 54.940 seconds (100% CPU, 7022377 Lips)
_Square = square(row(1, 24, 14, 2, 25), row(16, 21, 5, 8, 20), row(13, 10, 18, 23, 11), row(4, 7, 15, 3, 6), row(17, 22, 12, 9, 19)),
Vars = [1, 24, 14, 2, 25, 16, 21, 5, 8|...].

(这是在装有 SWI-Prolog 6.0.0 的旧机器上进行的。在装有 SWI-Prolog 7.2.3 的较新机器上,它的运行速度大约是原来的两倍,但这还不足以击败明显的指数复杂性。)

这里使用的部分解决方案来自https://www.nurkiewicz.com/2018/09/brute-forcing-seemingly-simple-number.html https://www.nurkiewicz.com/2018/09/brute-forcing-seemingly-simple-number.html

所以,我的问题是:如何加快以下 CLP(FD) 程序的速度?

额外感谢的附加问题:是否有一个特定的标签参数可以显着加快这一搜索速度,如果是这样,我如何才能有根据地猜测它可能是哪一个?

:- use_module(library(clpfd)).

% width of the square board
n(5).


% set up a term square(row(...), ..., row(...))
square(Square, N) :-
    length(Rows, N),
    maplist(row(N), Rows),
    Square =.. [square | Rows].

row(N, Row) :-
    functor(Row, row, N).


% Entry is the entry at 1-based coordinates (X, Y) on the board. Fails if X
% or Y is an invalid coordinate.
square_coords_entry(Square, (X, Y), Entry) :-
    n(N),
    0 < Y, Y =< N,
    arg(Y, Square, Row),
    0 < X, X =< N,
    arg(X, Row, Entry).


% Constraint is a CLP(FD) constraint term relating variable Var and the
% previous variable at coordinates (X, Y). X and Y may be arithmetic
% expressions. If X or Y is an invalid coordinate, this predicate succeeds
% with a trivially false Constraint.
square_var_coords_constraint(Square, Var, (X, Y), Constraint) :-
    XValue is X,
    YValue is Y,
    (   square_coords_entry(Square, (XValue, YValue), PrevVar)
    ->  Constraint = (Var #= PrevVar + 1)
    ;   Constraint = (0 #= 1) ).


% Compute and post constraints for variable Var at coordinates (X, Y) on the
% board. The computed constraint expresses that Var is 1, or it is one more
% than a variable located three steps in one of the cardinal directions or
% two steps along a diagonal.
constrain_entry(Var, Square, X, Y) :-
    square_var_coords_constraint(Square, Var, (X - 3, Y), C1),
    square_var_coords_constraint(Square, Var, (X + 3, Y), C2),
    square_var_coords_constraint(Square, Var, (X, Y - 3), C3),
    square_var_coords_constraint(Square, Var, (X, Y + 3), C4),
    square_var_coords_constraint(Square, Var, (X - 2, Y - 2), C5),
    square_var_coords_constraint(Square, Var, (X + 2, Y - 2), C6),
    square_var_coords_constraint(Square, Var, (X - 2, Y + 2), C7),
    square_var_coords_constraint(Square, Var, (X + 2, Y + 2), C8),
    Var #= 1 #\/ C1 #\/ C2 #\/ C3 #\/ C4 #\/ C5 #\/ C6 #\/ C7 #\/ C8.


% Compute and post constraints for the entire board.
constrain_square(Square) :-
    n(N),
    findall(I, between(1, N, I), RowIndices),
    maplist(constrain_row(Square), RowIndices).

constrain_row(Square, Y) :-
    arg(Y, Square, Row),
    Row =.. [row | Entries],
    constrain_entries(Entries, Square, 1, Y).

constrain_entries([], _Square, _X, _Y).
constrain_entries([E|Es], Square, X, Y) :-
    constrain_entry(E, Square, X, Y),
    X1 is X + 1,
    constrain_entries(Es, Square, X1, Y).


% The core relation: Square is a puzzle board, Vars a list of all the
% entries on the board in row-major order.
number_puzzle_(Square, Vars) :-
    n(N),
    square(Square, N),
    constrain_square(Square),
    term_variables(Square, Vars),
    Limit is N * N,
    Vars ins 1..Limit,
    all_different(Vars).

首先:

这里发生了什么?

要了解发生了什么,请查看以下内容后记定义让我们可视化搜索:



/n 5 def

340 n div dup scale
-0.9 0.1 translate % leave room for line strokes

/Palatino-Roman 0.8 selectfont

/coords { n exch sub translate } bind def

/num { 3 1 roll gsave coords 0.5 0.2 translate
    5 string cvs dup stringwidth pop -2 div 0 moveto show
    grestore } bind def

/clr { gsave coords 1 setgray 0 0 1 1 4 copy rectfill
     0 setgray 0.02 setlinewidth rectstroke grestore} bind def

1 1 n { 1 1 n { 1 index clr } for pop } for
  

这些定义为您提供了两个过程:

  • clr清除一个正方形
  • num在正方形上显示数字。

例如,如果您将这些定义保存到tour.ps然后调用 PostScript 解释器鬼脚本 with:

gs -r72 -g350x350 tour.ps

然后输入以下指令:



1 2 3 num
1 2 clr
2 3 4 num
  

you get:

PostScript 是一种用于可视化搜索过程的出色编程语言,我还建议您查看一下后记 /questions/tagged/postscript了解更多信息。

我们可以轻松修改您的程序以发出合适的 PostScript 指令,让我们直接观察搜索。我强调相关的补充:



constrain_entries([], _Square, _X, _Y).
constrain_entries([E|Es], Square, X, Y) :-
    freeze(E, postscript(X, Y, E)),
    constrain_entry(E, Square, X, Y),
    X1 #= X + 1,
    constrain_entries(Es, Square, X1, Y).

postscript(X, Y, N) :- format("~w ~w ~w num\n", [X,Y,N]).
postscript(X, Y, _) :- format("~w ~w clr\n", [X,Y]), false.
  

我也冒昧地改变了(is)/2 to (#=)/2使程序更加通用。

假设您将 PostScript 定义保存在tour.ps和你的 Prolog 程序tour.pl,以下 SWI-Prolog 和 Ghostscript 的调用说明了这种情况:

swipl -g "number_puzzle_(_, Vs), label(Vs)" tour.pl | gs -g350x350 -r72 tour.ps -dNOPROMPT

例如,我们在突出显示的位置看到大量回溯:

然而,根本问题已经完全存在于其他地方:

突出显示的方块都不是有效的移动!

From this, we see that your current formulation does not—at least not sufficiently early—let the solver recognize when a partial assignment cannot be completed to a solution! This is bad news, since failure to recognize inconsistent assignments often leads to unacceptable performance. For example, in order to correct the 1 &rightarrow; 3 transition (which can never occur in this way, yet is already one of the first choices made in this case), the solver would have to backtrack over approximately 8 squares, after enumerating—as a very rough estimate—258 = 152587890625 partial solutions, and then start all over at only the second position in the board.

在约束文献中,这种回溯被称为殴打。意味着重复的失败由于同样的原因.

这怎么可能?您的模型似乎是正确的,可以用来检测解决方案。那挺好的!然而,一个好的约束公式不仅可以识别解,还可以识别解。快速检测部分赋值cannot完成解决方案。这使得求解器能够有效地修剪搜索,正是在这个重要方面,您当前的公式存在不足。造成这种情况的原因之一与约束传播有关具体化的限制您正在使用的。特别是,考虑以下查询:



?- (X + 1 #= 3) #<==> B, X #\= 2.
  

直觉上,我们期望B = 0。但事实并非如此!相反,我们得到:



X in inf..1\/3..sup,
X+1#=_3840,
_3840#=3#B,
B in 0..1.
  

因此,求解器不会非常强烈地传播具体化平等。也许应该如此!仅够用feedbackProlog 实践者将判断约束求解器的这个区域是否应该改变,可能会牺牲一点速度来换取更强的传播。此反馈的高度相关性是我建议只要有机会(即每次推理时)就使用 CLP(FD) 约束的原因之一integers.

对于这个特殊情况,我可以告诉您,在这个意义上使求解器变得更强并不会产生太大的影响。本质上,您最终会得到一个核心问题仍然存在的主板版本,其中许多转换(其中一些在下面突出显示)在任何解决方案中都无法发生:

解决核心问题

我们应该消除回溯的原因其核心。为了修剪搜索,我们必须尽早识别不一致的(部分)分配。

直觉上,我们正在寻找一个互联旅游,并希望在明确旅行无法按预期方式继续时立即原路返回。

为了实现我们想要的目标,我们至少有两个选择:

  1. 更改分配策略以考虑连通性
  2. 以更强烈地考虑连通性的方式对问题进行建模。

方案一:分配策略

CLP(FD) 约束的一个主要吸引力在于它们让我们decouple来自搜索的任务描述。当使用CLP(FD)约束时,我们经常通过以下方式执行搜索label/1 or labeling/2。但是,我们可以自由地为变量赋值以任何我们想要的方式。如果我们遵循(就像您所做的那样)将“约束发布”部分放入其自己的谓词(称为核心关系.

例如,这里有一个custom确保旅行始终保持连接的分配策略:



allocation(Vs) :-
        length(Vs, N),
        numlist(1, N, Ns),
        maplist(member_(Vs), Ns).

member_(Es, E) :- member(E, Es).
  

通过这个策略,我们从头开始得到了 5×5 实例的解决方案:



?- number_puzzle_(Square, Vars), time(allocation(Vars)).
% 5,030,522 inferences, 0.907 CPU in 0.913 seconds (99% CPU, 5549133 Lips)
Square = square(row(1, 8, 5, 2, 11), ...),
Vars = [1, 8, 5, 2, 11, 16, 21, 24, 15|...] 
  

该策略有多种修改值得尝试。例如,当允许多个方格时,我们可以尝试通过考虑方格的数量来做出更明智的选择剩余域元素的正方形。我将尝试此类改进视为一项挑战。

从标准标签策略来看,min在这种情况下,标签选项实际上与此策略非常相似,并且实际上它也找到了 5×5 情况的解决方案:



?- number_puzzle_(Square, Vars), time(labeling([min], Vars)).
% 22,461,798 inferences, 4.142 CPU in 4.174 seconds (99% CPU, 5422765 Lips)
Square = square(row(1, 8, 5, 2, 11), ...),
Vars = [1, 8, 5, 2, 11, 16, 21, 24, 15|...] .
  

然而,即使是拟合分配策略也无法完全补偿弱约束传播。对于 10×10 实例,经过一番搜索后,棋盘看起来像这样min option:

请注意,我们还必须调整n在 PostScript 代码中将其可视化为预期的效果。

理想情况下,我们应该以能够从强传播中受益的方式制定任务,然后also使用良好的分配策略。

选项2:改造

良好的 CLP 公式可以传播尽可能强烈(在可接受的时间内)。因此,我们应该努力使用约束,使求解器能够更容易地推理任务最重要的要求。在这个具体的例子中,这意味着我们应该尝试找到一个更合适的表述来表达目前所表达的具体化约束的析取,如上所示,不允许太多的传播。理论上,约束求解器可以自动识别此类模式。然而,这对于许多用例来说是不切实际的,因此我们有时必须通过手动尝试几种有前途的公式来进行实验。不过,同样在这种情况下:有了应用程序程序员的足够反馈,这种情况更有可能得到改进和处理!

我现在使用 CLP(FD) 约束circuit/1明确我们正在寻找哈密​​顿电路在特定的图表中。该图表示为整数变量列表,其中每个元素表示其位置接班人在列表中。

例如,一个包含 3 个元素的列表恰好包含 2 个哈密顿回路:



?- Vs = [_,_,_], circuit(Vs), label(Vs).
Vs = [2, 3, 1] ;
Vs = [3, 1, 2].
  

I use circuit/1描述解决方案封闭式游览。这意味着,如果我们找到这样的解决方案,那么我们可以通过从找到的游览中最后一个方格开始的有效移动从头开始:



n_tour(N, Vs) :-
        L #= N*N,
        length(Vs, L),
        successors(Vs, N, 1),
        circuit(Vs).

successors([], _, _).
successors([V|Vs], N, K0) :-
        findall(Num, n_k_next(N, K0, Num), [Next|Nexts]),
        foldl(num_to_dom, Nexts, Next, Dom),
        V in Dom,
        K1 #= K0 + 1,
        successors(Vs, N, K1).

num_to_dom(N, D0, D0\/N).

n_x_y_k(N, X, Y, K) :- [X,Y] ins 1..N, K #= N*(Y-1) + X.

n_k_next(N, K, Next) :-
        n_x_y_k(N, X0, Y0, K),
        (   [DX,DY] ins -2 \/ 2
        ;   [DX,DY] ins -3 \/ 0 \/ 3,
            abs(DX) + abs(DY) #= 3
        ),
        [X,Y] ins 1..N,
        X #= X0 + DX,
        Y #= Y0 + DY,
        n_x_y_k(N, X, Y, Next),
        label([DX,DY]).
  

请注意现在如何将可接受的后继者表示为域elements,减少约束的数量并完全消除具体化的需要。最重要的是,现在在搜索过程中的每个点都会自动考虑并强制执行预期的连接性。谓词n_x_y_k/4涉及(X,Y)坐标来列出索引。您可以通过更改此程序轻松地适应其他任务(例如,骑士之旅)n_k_next/3。我将概括留给open旅行作为一种挑战。

这里有一些额外的定义,让我们print更易读的形式的解决方案:



:- set_prolog_flag(double_quotes, chars).

print_tour(Vs) :-
        length(Vs, L),
        L #= N*N, N #> 0,
        length(Ts, N),
        tour_enumeration(Vs, N, Es),
        phrase(format_string(Ts, 0, 4), Fs),
        maplist(format(Fs), Es).

format_(Fs, Args, Xs0, Xs) :- format(chars(Xs0,Xs), Fs, Args).

format_string([], _, _) --> "\n".
format_string([_|Rest], N0, I) -->
        { N #= N0 + I },
        "~t~w~", call(format_("~w|", [N])),
        format_string(Rest, N, I).

tour_enumeration(Vs, N, Es) :-
        length(Es, N),
        maplist(same_length(Es), Es),
        append(Es, Ls),
        foldl(vs_enumeration(Vs, Ls), Vs, 1-1, _).

vs_enumeration(Vs, Ls, _, V0-E0, V-E) :-
        E #= E0 + 1,
        nth1(V0, Ls, E0),
        nth1(V0, Vs, V).
  

在具有强传播的配方中,预定义的ff搜索策略通常是一个好的策略。事实上,它可以让我们在商用机器上几秒钟内解决整个任务,即原始的 10×10 实例:



?- n_tour(10, Vs),
   time(labeling([ff], Vs)),
   print_tour(Vs).
% 5,642,756 inferences, 0.988 CPU in 0.996 seconds (99% CPU, 5710827 Lips)
   1  96  15   2  97  14  80  98  13  79
  93  29  68  94  30  27  34  31  26  35
  16   3 100  17   4  99  12   9  81  45
  69  95  92  28  67  32  25  36  33  78
  84  18   5  83  19  10  82  46  11   8
  91  23  70  63  24  37  66  49  38  44
  72  88  20  73   6  47  76   7  41  77
  85  62  53  86  61  64  39  58  65  50
  90  22  71  89  21  74  42  48  75  43
  54  87  60  55  52  59  56  51  40  57
Vs = [4, 5, 21, 22, 8, 3, 29, 26, 6|...] 
  

为了获得最佳性能,我建议您也尝试使用其他 Prolog 系统。商业级 CLP(FD) 系统的效率通常是购买 Prolog 系统的重要原因。

还要注意,这绝不是该任务唯一有前途的 Prolog 甚至 CLP(FD) 表述,我将其他表述视为一项挑战。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

针对数字板难题的优化 CLP(FD) 求解器 的相关文章

  • 在 dll 中嵌入 prolog 引擎

    我最近一直在开发一个嵌入 prolog 推理引擎的 C 应用程序 正如标题中所述 我现在尝试生成一个 DLL 而不是可执行文件 以便我可以在另一个项目中使用它 由于我是 DLL 开发的新手 我想我可以从一个小例子开始 我有3个文件 like
  • 如何声明两个列表具有相同的长度?

    我需要知道如何比较 Prolog 中两个列表的长度 这是我到目前为止所拥有的 sum N1 N2 checklength N1 N2 checklength N1 N2 L1 is length N1 What L2 is length N
  • Prolog 程序从列表中删除每个第 n 个元素

    您能帮我解决以下问题吗 编写三元谓词delete nth从列表中删除每个第 n 个元素 样本运行 delete nth a b c d e f 2 L L a c e false delete nth a b c d e f 1 L L f
  • 展平列表

    尝试解决练习 07http www ic unicamp br meidanis courses mc336 2009s2 prolog problemas http www ic unicamp br meidanis courses m
  • 在 Prolog 中编辑 Eliza 聊天机器人

    我一直在努力尝试在 Prolog 中编辑 Eliza 聊天机器人 每次我尝试编辑某些内容时 都会出现新的错误 它是否受到任何形式的编辑保护 我使用 SWI prolog 编辑器进行编辑 问题是我试图在没有完全理解代码的情况下最小化代码 我正
  • Prolog,如何在 write() 中显示多个输出

    go match Mn Fn write Matching Result nl write Mn write match with write Fn match Mn1 Fn1 person may female 25 blue perso
  • 针对数字板难题的优化 CLP(FD) 求解器

    考虑问题从https puzzling stackexchange com questions 20238 explore the square with 100 hops https puzzling stackexchange com
  • 如何在Prolog中编写cmp_list/3函数?

    Write a predicate cmp list 3 the first 2 arguments are 2 lists and the last one is Comparison which means ge lt le or gt
  • Prolog 管线任务

    我有一项任务是在序言中制作一张简化的地铁地图 其中一部分要求制定一项规则来检查两个车站是否在同一条线上 我有一条规则 但它似乎不起作用 这就是我到目前为止所拥有的 adjacent nh lg central 4 adjacent lg o
  • Prolog 中的匹配元组

    为什么Prolog匹配 X Xs 包含更多元素的元组 一个例子 test2 X Xs write X nl test2 Xs test2 X write X nl test
  • 求解序言中极其简单的方程:A = B + C?

    我有一个非常简单的方程 我希望能够在序言中求解 A B C 我希望能够编写一个谓词来表达这种关系 它可以处理任何一个未实例化的参数 无需推广到更复杂的关系或方程 myEquation A B C something 我可以使用以下语义进行调
  • 在 Prolog 中动态拆分列表

    我从序言开始几周 但我看到了更深入的操作列表的递归谓词的构造 我的问题是 是否可以构建一个谓词 将给定列表拆分为给定数量的其他列表 比如我想象的 split H T NumberLists Lists 递归实现 split 1 2 3 4
  • 在列表列表中查找形状

    节目说明 该计划的目的 我的程序旨在计算 20X15 大小的平面中形状的位置 我有一个形状列表 其中包含形状类型 其 ID 半径或高度以及其在平面上的预期 X Y 位置 我有一个不同的二元运算列表 仅包含形状类型 其 id 及其与另一个形状
  • 转换句子会产生无限循环 - 但如何转换呢?

    我不明白这是哪里出了问题 请注意 我对 Prolog 很陌生 我确信我错过了一些东西 只是不知道那可能是什么 有人可以帮我吗 谢谢 这是我的代码 printSentence printSentence W write W write nl
  • 计算序言中列表的排列

    在 序言艺术 第二版中有一个问题 您应该定义一个谓词 Even permutation Xs Ys 和类似的奇数排列 当您查询时 例如 Even permutation 1 2 3 2 3 1 和 odd permutation 1 2 3
  • F# 和模糊逻辑

    我知道这可能听起来很奇怪 但我想知道 Microsoft Visual F 正在进入的这个新世界中的一件事 这种语言有很多应用 我要学习 关于解析 函数式编程 结构化编程 但是人工智能呢 模糊逻辑有什么应用吗 F 是一种适合模糊逻辑应用程序
  • 如何找到排列的索引

    index List Idx Predicate will get List with permutation and I want to know index of permutation For example index 4 1 3
  • Prolog中如何选择bagof、setof和findall

    如何在 bagof setof 和 findall 之间做出选择 有什么重要的区别吗 哪个最常用 哪个最安全 感谢您的评论 回答 我检查了SWI Prolog 手册页findall 3 http www swi prolog org pld
  • 使用 prolog 添加另外两次出现

    我有一个清单 a b a a a c c 我需要为每个元素添加两次以上的出现 最终结果应该是这样的 a a a b b b a a a a a c c c c 如果列表中有一个与下一个项目相同的项目 那么它会继续下去 直到出现一个新项目 当
  • 一次性删除不正确的后续解决方案

    我有一个谓词 它找到正确的解决方案 但随后又找到不正确的解决方案 data D data threshold nonredundantbumps D 5 Bs write D 3 6 7 8 2 4 5 6 9 4 7 3 D 3 6 7

随机推荐