3

我试图用 clpfd 解决“逃离 Zurg”的问题。https://web.engr.oregonstate.edu/~erwig/papers/Zurg_JFP04.pdf 玩具从左侧开始,向右移动。这就是我所拥有的:

:-use_module(library(clpfd)).

toy(buzz,5).
toy(woody,10).
toy(res,20).
toy(hamm,25).

%two toys cross, the time is the max of the two.
cross([A,B],Time):-
  toy(A,T1),
  toy(B,T2),
  dif(A,B),
  Time#=max(T1,T2).
%one toy crosses
cross(A,T):-
  toy(A,T).

%Two toys travel left to right
solve_L(Left,Right,[l_r(A,B,T)|Moves]):-
  select(A,Left,L1),
  select(B,L1,Left2),
  cross([A,B],T),
  solve_R(Left2,[A,B|Right],Moves).

%One toy has to return with the flash light
solve_R([],_,[]).
solve_R(Left,Right,[r_l(A,empty,T)|Moves]):-
  select(A,Right,Right1),
  cross(A,T),
  solve_L([A|Left],Right1,Moves).

solve(Moves,Time):-
   findall(Toy,toy(Toy,_),Toys),
   solve_L(Toys,_,Moves),
   all_times(Moves,Times),
   sum(Times,#=,Time).

all_times([],[]).
all_times(Moves,[Time|Times]):-
  Moves=[H|Tail],
  H=..[_,_,_,Time],
  all_times(Tail,Times).

查询?-solve(M,T)?-solve(Moves,T), labeling([min(T)],[T]).我得到一个解决方案,但没有一个 =< 60。(我也看不到一个..)我将如何使用 clpfd 执行此操作?还是最好使用链接中的方法?

仅供参考:我还发现了这个http://www.metalevel.at/zurg/zurg.html 它有一个 DCG 解决方案。其中内置了约束 Time=<60,它没有找到最低时间。

4

4 回答 4

5

这是基于您链接到的代码的 CLP(FD) 版本。

主要区别在于,在这个版本中,Limit是一个参数而不是硬编码值。此外,它还使用 CLP(FD) 约束的灵活性来表明,与低级算术相比,您可以在使用约束时更自由地重新排序目标,并以更加声明性的方式推理代码:

:- use_module(library(clpfd)).

toy_time(buzz,   5).
toy_time(woody, 10).
toy_time(rex,   20).
toy_time(hamm,  25).

moves(Ms, Limit) :-
    phrase(moves(state(0,[buzz,woody,rex,hamm],[]), Limit), Ms).

moves(state(T0,Ls0,Rs0), Limit) -->
    [left_to_right(Toy1,Toy2)],
    { T1 #= T0 + max(Time1,Time2), T1 #=< Limit,
      select(Toy1, Ls0, Ls1), select(Toy2, Ls1, Ls2),
      Toy1 @< Toy2,
      toy_time(Toy1, Time1), toy_time(Toy2, Time2) },
    moves_(state(T1,Ls2,[Toy1,Toy2|Rs0]), Limit).

moves_(state(_,[],_), _)         --> [].
moves_(state(T0,Ls0,Rs0), Limit) -->
    [right_to_left(Toy)],
    { T1 #= T0 + Time, T1 #=< Limit,
      select(Toy, Rs0, Rs1),
      toy_time(Toy, Time) },
    moves(state(T1,[Toy|Ls0],Rs1), Limit).

使用示例,使用迭代深化首先找到最快的解决方案:

?- 长度(_,限制),移动(Ms,限制)。
限制 = 60,
Ms = [left_to_right(buzz, woody), right_to_left(buzz), left_to_right(hamm, rex), right_to_left(woody), left_to_right(buzz, woody)];
限制 = 60,
Ms = [left_to_right(buzz, woody), right_to_left(woody), left_to_right(hamm, rex), right_to_left(buzz), left_to_right(buzz, woody)];
限制 = 61,
Ms = [left_to_right(buzz, woody), right_to_left(buzz), left_to_right(hamm, rex), right_to_left(woody), left_to_right(buzz, woody)];
等等

请注意,此版本使用 CLP(FD) 约束(用于修剪和算术)和内置 Prolog 回溯的组合,这种组合是完全合法的。在某些情况下,全局约束(如automaton/8CapelliC 所提到的)可以完整地表达一个问题,但是将约束与正常回溯相结合对于许多任务来说也是一个很好的策略。

事实上,仅仅发布 CLP(FD) 约束通常是不够的:您通常还需要labeling/2在 CLP(FD) 的情况下提供的(回溯)搜索,以获得具体的解决方案。因此,如果您成功地仅使用 CLP(FD) 约束确定性地表达问题,这种迭代深化类似于labeling/2否则将执行的搜索。

很好,我们还可以展示:

?- 限制 #< 60,移动(女士,限制)。
错误的。

编辑automaton/8:由于对 CLP(FD) 约束感兴趣的用户似乎几乎无法抑制对它的渴望,这很好,我还为您创建了一个具有这个强大的全局约束的解决方案。如果您觉得这很有趣,也请支持@CapelliC 的回答,因为他有最初的想法automaton/8用于此。这个想法是让一个或两个玩具的每个可能(和合理的)运动对应一个唯一的整数,这些运动会引起自动机不同状态之间的转换。请注意,闪光灯的侧面在状态中也起着重要作用。此外,我们为每条弧线配备了一个算术表达式,以跟踪到目前为止所用的时间。请尝试?- arc(_, As).看看这个自动机的弧线。

:- use_module(library(clpfd)).

toy_time(b,  5).
toy_time(w, 10).
toy_time(r, 20).
toy_time(h, 25).

toys(Toys) :- setof(Toy, T^toy_time(Toy, T), Toys).

arc0(arc0(S0,M,S)) :-
    state(S0),
    state0_movement_state(S0, M, S).

arcs(V, Arcs) :-
    findall(Arc0, arc0(Arc0), Arcs0),
    movements(Ms),
    maplist(arc0_arc(V, Ms), Arcs0, Arcs).

arc0_arc(C, Ms, arc0(S0,M,S), arc(S0, MI, S, [C+T])) :-
    movement_time(M, T),
    nth0(MI, Ms, M).

movement_time(left_to_right(Toy), Time) :- toy_time(Toy, Time).
movement_time(left_to_right(T1,T2), Time) :-
    Time #= max(Time1,Time2),
    toy_time(T1, Time1),
    toy_time(T2, Time2).
movement_time(right_to_left(Toy), Time) :- toy_time(Toy, Time).


state0_movement_state(lrf(Ls0,Rs0,left), left_to_right(T), lrf(Ls,Rs,right)) :-
    select(T, Ls0, Ls),
    sort([T|Rs0], Rs).
state0_movement_state(lrf(Ls0,Rs0,left), left_to_right(T1,T2), S) :-
    state0_movement_state(lrf(Ls0,Rs0,left), left_to_right(T1), lrf(Ls1,Rs1,_)),
    state0_movement_state(lrf(Ls1,Rs1,left), left_to_right(T2), S),
    T1 @< T2.
state0_movement_state(lrf(Ls0,Rs0,right), right_to_left(T), lrf(Ls,Rs,left)) :-
    select(T, Rs0, Rs),
    sort([T|Ls0], Ls).

movements(Moves) :-
    toys(Toys),
    findall(Move, movement(Toys, Move), Moves).

movement(Toys, Move) :-
    member(T, Toys),
    (   Move = left_to_right(T)
    ;   Move = right_to_left(T)
    ).
movement(Toys0, left_to_right(T1, T2)) :-
    select(T1, Toys0, Toys1),
    member(T2, Toys1),
    T1 @< T2.

state(lrf(Lefts,Rights,Flash)) :-
    toys(Toys),
    phrase(lefts(Toys), Lefts),
    foldl(select, Lefts, Toys, Rights),
    ( Flash = left ; Flash = right ).

lefts([]) --> [].
lefts([T|Ts]) --> ( [T] | [] ), lefts(Ts).

现在,终于,我们终于可以使用automaton/8我们非常渴望的解决方案了,我们真正认为值得打出“CLP(FD)”的旗帜,并与以下min/1选项一起狂欢labeling/2

?- time((arcs(C, Arcs),
         length(Vs, _),
         automaton(Vs, _, Vs, [source(lrf([b,h,r,w],[],left)),
                               sink(lrf([],[b,h,r,w],right))],
                   Arcs, [C], [0], [Time]),
         labeling([min(Time)], Vs))).

产生:

857,542 次推理,0.097 CPU 在 0.097 秒内(100% CPU,8848097 唇)
弧线 = [...],
时间 = 60,
Vs = [10, 1, 11, 7, 10] ;
等等

我将这些解决方案转换为可读的状态转换作为一个简单的练习(约 3 行代码)。

为了更加满意,这比使用普通 Prolog 的原始版本要快得多,为此我们有:

?-时间((长度(_,限制),移动(毫秒,限制)))。
1,666,522 次推理,0.170 CPU 在 0.170 秒内(100% CPU,9812728 唇)

这个故事的寓意:如果您的直接 Prolog 解决方案需要超过十分之一秒才能产生解决方案,那么您最好学习如何使用最复杂和最强大的全局约束之一,以便将运行时间缩短几毫秒!:-)

然而,更严肃的一点是,这个例子表明约束传播可以很快得到回报,即使对于相对较小的搜索空间也是如此。当使用 CLP(FD) 解决更复杂的搜索问题时,您可以获得更大的相对收益。

请注意,虽然第二个版本,虽然它在某种意义上更全局地传播约束,但缺少一个与传播和修剪也相关的重要特征:以前,我们能够直接使用程序来证明没有解决方案需要更少的时间超过 60 分钟,使用直接且自然的查询(?- Limit #< 60, moves(Ms, Limit).失败)。这只是隐含地从第二个程序得出,因为我们知道,在其他条件不变的情况下,更长的列表最多可以增加花费的时间。不幸的是,孤立的电话length/2没有得到备忘录。

另一方面,第二个版本能够证明一些在某种意义上至少同样令人印象深刻的东西,而且它比第一个版本更有效、更直接:甚至不需要构造一个明确的解决方案,我们可以使用第二个版本表明任何解决方案(如果有的话)至少需要5 次交叉:

?- time((arcs(C, Arcs),
         length(Vs, L),
         automaton(Vs, _, Vs, [source(lrf([b,h,r,w],[],left)),
                               sink(lrf([],[b,h,r,w],right))],
         Arcs, [C], [0], [Time]))).

产生:

331,495 次推理,0.040 秒内 0.040 CPU(100% CPU,8195513 唇)
...,
L = 5
... .

这仅通过约束传播起作用,不涉及任何labeling/2

于 2015-10-07T14:13:16.633 回答
3

我认为用 CLPFD 建模这个难题可以用 automaton/8 完成。在 Prolog 我会写

escape_zurg(T,S) :-
    aggregate(min(T,S), (
     solve([5,10,20,25], [], S),
     sum_timing(S, T)), min(T,S)).

solve([A, B], _, [max(A, B)]).
solve(L0, R0, [max(A, B), C|T]) :-
    select(A, L0, L1),
    select(B, L1, L2),
    append([A, B], R0, R1),
    select(C, R1, R2),
    solve([C|L2], R2, T).

sum_timing(S, T) :-
    aggregate(sum(E), member(E, S), T).

产生这个解决方案

?- escape_zurg(T,S).
T = 60,
S = [max(5, 10), 5, max(20, 25), 10, max(10, 5)].

编辑

好吧,automaton/8 远远超出我的能力范围......让我们开始更简单:什么可以是 state 的简单表示?在左/右我们有 4 个插槽,可以是空的:所以

escape_clpfd(T, Sf) :-
    L0 = [_,_,_,_],
    Zs = [0,0,0,0],
    L0 ins 5\/10\/20\/25,
    all_different(L0),
    ...

现在,由于问题很简单,我们可以“硬编码”状态变化

...
lmove(L0/Zs, 2/2, L1/R1, T1), rmove(L1/R1, 1/3, L2/R2, T2),
lmove(L2/R2, 3/1, L3/R3, T3), rmove(L3/R3, 2/2, L4/R4, T4),
lmove(L4/R4, 4/0, Zs/ _, T5),
...

第一个lmove/4必须将 2 个元素从左向右移动,完成后,我们将在左侧有 2 个零,在右侧有 2 个零。时间 (T1) 将是max(A,B),此时 A,B 是不认识的。 rmove/4类似,但将在 T2 中“返回”唯一的元素(隐身),它将从右向左移动。我们正在对进化进行编码,断言每一侧的 0 的数量(似乎不难概括)。

让我们完成:

...
T #= T1 + T2 + T3 + T4 + T5,
Sf = [T1,T2,T3,T4,T5].

现在, rmove/4 更简单,所以让我们编写代码:

rmove(L/R, Lz/Rz, Lu/Ru, M) :-
    move_one(R, L, Ru, Lu, M),
    count_0s(Ru, Rz),
    count_0s(Lu, Lz).

它遵循 move_one/5 的实际工作,然后应用我们在上面硬编码的数字约束:

count_0s(L, Z) :-
    maplist(is_0, L, TF),
    sum(TF, #=, Z).

is_0(V, C) :- V #= 0 #<==> C.

is_0 /2将空槽条件具体化,即使真值可数。值得测试一下:

?- count_0s([2,1,1],X).
X = 0.

?- count_0s([2,1,C],1).
C = 0.

?- count_0s([2,1,C],2).
false.

在 CLP(FD) 中编码 move_one/5 似乎很困难。这里 Prolog 的不确定性似乎真的很合适......

move_one(L, R, [Z|Lt], [C|Rt], C) :-
    select(C, L, Lt), is_0(C, 0),
    select(Z, R, Rt), is_0(Z, 1).

select/3 它是一个纯谓词,当需要标记时 Prolog 将回溯......

没有最小化,但在我们得到解决方案后很容易添加。到目前为止,对我来说,一切似乎都是“合乎逻辑的”。但是当然...

?- escape_clpfd(T, S).
false.

所以,这里是龙...

?- spy(lmove),escape_clpfd(T, S).
% Spy point on escape_zurg:lmove/4
 * Call: (9) escape_zurg:lmove([_G12082{clpfd = ...}, _G12164{clpfd = ...}, _G12246{clpfd = ...}, _G12328{clpfd = ...}]/[0, 0, 0, 0], 2/2, _G12658/_G12659, _G12671) ?  creep
   Call: (10) escape_zurg:move_one([_G12082{clpfd = ...}, _G12164{clpfd = ...}, _G12246{clpfd = ...}, _G12328{clpfd = ...}], [0, 0, 0, 0], _G12673, _G12674, _G12661) ? sskip

...等等等等

抱歉,如果我有空闲时间调试,将发布解决方案...

编辑有几个错误......这个 lmove/4

lmove(L/R, Lz/Rz, Lu/Ru, max(A, B)) :-
    move_one(L, R, Lt, Rt, A),
    move_one(Lt, Rt, Lu, Ru, B),
    count_0s(Lu, Lz),
    count_0s(Ru, Rz).

至少我们开始得到解决方案(从外部添加变量到标签的接口......)

escape_clpfd(T, Sf, L0) :- ...

?- escape_clpfd(T, S, Vs), label(Vs).
T = 85,
S = [max(5, 10), 10, max(10, 20), 20, max(20, 25)],
Vs = [5, 10, 20, 25] ;
T = 95,
S = [max(5, 10), 10, max(10, 25), 25, max(25, 20)],
Vs = [5, 10, 25, 20] ;
...

编辑

上面的代码有效,但速度非常慢:

?- time((escape_clpfd(60, Sf, L0),label(L0))).
% 15,326,054 inferences, 5.466 CPU in 5.485 seconds (100% CPU, 2803917 Lips)
Sf = [max(5, 10), 10, max(20, 25), 5, max(5, 10)],
L0 = [5, 10, 20, 25] 

将此更改为 move_one/5:

move_one([L|Ls], [R|Rs], [R|Ls], [L|Rs], L) :-
    L #\= 0,
    R #= 0.
move_one([L|Ls], [R|Rs], [L|Lu], [R|Ru], E) :-
    move_one(Ls, Rs, Lu, Ru, E).

我有更好的表现:

?- time((escape_clpfd(60, Sf, L0),label(L0))).
% 423,394 inferences, 0.156 CPU in 0.160 seconds (97% CPU, 2706901 Lips)
Sf = [max(5, 10), 5, max(20, 25), 10, max(5, 10)],
L0 = [5, 10, 20, 25] 

然后,添加到 lmove/4

... A #< B, ...

我明白了

% 233,953 inferences, 0.089 CPU in 0.095 seconds (94% CPU, 2621347 Lips)
Sf = [max(5, 10), 5, max(20, 25), 10, max(5, 10)],

整体它仍然比我的纯 Prolog 解决方案慢很多......

编辑

其他小的改进:

?- time((escape_clpfd(60, Sf, L0),maplist(#=,L0,[5,10,20,25]))).
% 56,583 inferences, 0.020 CPU in 0.020 seconds (100% CPU, 2901571 Lips)
Sf = [max(5, 10), 5, max(20, 25), 10, max(5, 10)],

其中 all_different/1 已替换为

...
chain(L0, #<),
...

另一个改进:将两边都计数为零是没有用的:在 lmove 和 rmove 中删除(任意)一侧,我们得到

% 35,513 inferences, 0.014 CPU in 0.014 seconds (100% CPU, 2629154 Lips)
Sf = [max(5, 10), 5, max(20, 25), 10, max(5, 10)],

编辑

只是为了好玩,这里是相同的纯(聚合除外)Prolog 解决方案,使用简单的确定性“提升”变量(礼貌 ' lifter '):

:- use_module(carlo(snippets/lifter)).

solve([A, B], _, [max(A, B)]).
solve(L0, R0, [max(A, B), C|T]) :-
    solve([C|select(B, select(A, L0, °), °)],
          select(C, append([A, B], R0, °), °),
          T).

顺便说一句,它相当快:

?- time(escape_zurg(T,S)).
% 50,285 inferences, 0.065 CPU in 0.065 seconds (100% CPU, 769223 Lips)
T = 60,
S = [max(5, 10), 5, max(20, 25), 10, max(10, 5)].

(绝对时间不太好,因为我正在运行为调试而编译的 SWI-Prolog)

于 2015-10-07T00:34:53.813 回答
1

我认为@mat 为我最初尝试做的事情提供了一个很好的答案,但我确实尝试并使用了 automaton/4,以及回溯搜索来添加弧线。这是我所得到的。但是我在调​​用ERROR: Arguments are not sufficiently instantiated时收到错误消息bridge/2。如果有人对此方法有任何评论或知道为什么会出现此错误,或者我使用automaton/4完全错误,请在此处发布!

fd_length(L, N) :-
  N #>= 0,
  fd_length(L, N, 0).

fd_length([], N, N0) :-
  N #= N0.
fd_length([_|L], N, N0) :-
  N1 is N0+1,
  N #>= N1,
fd_length(L, N, N1).

left_to_right_arc(L0,R0,Arc):-
  LenL#=<4,
  fd_length(L0,LenL),
  LenR #=4-LenL,
  fd_length(R0,LenR),
  L0 ins 5\/10\/20\/25,
  R0 ins 5\/10\/20\/25,
  append(L0,R0,All),
  all_different(All),
  Before =[L0,R0],
  select(A,L0,L1),
  select(B,L1,L2),
  append([A,B],R0,R1),
  After=[L2,R1],
  Cost #=max(A,B),
  Arc =arc(Before,Cost,After).

right_to_left_arc(L0,R0,Arc):-
  LenL#=<4,
  fd_length(L0,LenL),
  LenR #=4-LenL,
  fd_length(R0,LenR),
  L0 ins 5\/10\/20\/25,
  R0 ins 5\/10\/20\/25,
  append(L0,R0,All),
  all_different(All),
  Before=[L0,R0],
  select(A,R0,R1),
  append([A],L0,L1),
  After=[L1,R1],
  Cost#=A,
  Arc =arc(After,Cost,Before).

pair_of_arcs(Arcs):-
  left_to_right_arc(_,_,ArcLR),
  right_to_left_arc(_,_,ArcRL),
  Arcs =[ArcLR,ArcRL].

pairs_of_arcs(Pairs):-
  L#>=1,
  fd_length(Pairs,L),
  once(maplist(pair_of_arcs,Pairs)).

bridge(Vs,Arcs):-
  pairs_of_arcs(Arcs),
  flatten(Arcs,FArcs),
  automaton(Vs,[source([[5,10,20,25],[]]),sink([[],[5,10,20,25]])],
      FArcs).
于 2015-10-07T14:31:09.060 回答
0

不是使用 CLP(FD) 的答案,而只是为了展示成本等于或低于 60 的该难题存在的两种解决方案(文本太大,无法放入评论)。

这个谜题有几种变体。在其示例中,Logtalk 包括一个searching/bridge.lgt具有不同字符集和相应时间的过桥。但是我们可以修补它来解决这个问题的变化(使用当前的 Logtalk git 版本):

?- set_logtalk_flag(complements, allow).
true.

?- {searching(loader)}.
...
% (0 warnings)
true.

?- create_category(patch, [complements(bridge)], [], [initial_state(start, ([5,10,20,25], left, [])), goal_state(end, ([], right, [5,10,20,25]))]).
true.

?- performance::init, bridge::initial_state(Initial), hill_climbing(60)::solve(bridge, Initial, Path, Cost), bridge::print_path(Path), performance::report.
5 10 20 25  lamp _|____________|_ 
20 25  _|____________|_ lamp 5 10 
5 20 25  lamp _|____________|_ 10 
5  _|____________|_ lamp 10 20 25 
5 10  lamp _|____________|_ 20 25 
 _|____________|_ lamp 5 10 20 25 
solution length: 6
state transitions (including previous solutions): 113
ratio solution length / state transitions: 0.05309734513274336
minimum branching degree: 1
average branching degree: 5.304347826086956
maximum branching degree: 10
time: 0.004001000000000032
Initial =  ([5, 10, 20, 25], left, []),
Path = [([5, 10, 20, 25], left, []),  ([20, 25], right, [5, 10]),  ([5, 20, 25], left, [10]),  ([5], right, [10, 20, 25]),  ([5, 10], left, [20, 25]),  ([], right, [5|...])],
Cost = 60 ;
5 10 20 25  lamp _|____________|_ 
20 25  _|____________|_ lamp 5 10 
10 20 25  lamp _|____________|_ 5 
10  _|____________|_ lamp 5 20 25 
5 10  lamp _|____________|_ 20 25 
 _|____________|_ lamp 5 10 20 25 
solution length: 6
state transitions (including previous solutions): 219
ratio solution length / state transitions: 0.0273972602739726
minimum branching degree: 1
average branching degree: 5.764705882352941
maximum branching degree: 10
time: 0.0038759999999999906
Initial =  ([5, 10, 20, 25], left, []),
Path = [([5, 10, 20, 25], left, []),  ([20, 25], right, [5, 10]),  ([10, 20, 25], left, [5]),  ([10], right, [5, 20, 25]),  ([5, 10], left, [20, 25]),  ([], right, [5|...])],
Cost = 60 ;
false.
于 2015-10-07T09:16:13.913 回答