在条件头中使用时如何处理整数暂停

How integer suspension will be handled when it is used in head of a condition

我对两个变量 AB 有以下条件:

[A,B] #:: 1..10,
(A #= 3) or (B #= 3),
((A #> 3 or B #>3) ->
     % expression 1
;
     % expression 2
)
%cntd

问题在第2行,求解器不知道AB的值,如何在不指定第 2 行的变量?

合理的做法是在求解器遍历变量的可能值时,根据变量的值决定该分支。 但是,正如我发现的那样,它在知道变量的值之前会经过其中一个表达式。防止这种情况的解决方案是什么?

有什么问题? 重要的注意事项是对 if-else 使用 ->(箭头)。当我们有表达式 S -> T ; U 时,S 将被计算,如果它包含一些变量可以在代码上有一些 side-effects。为了更清楚,尝试运行一些例子:

?-[A,B] #:: 1..10,
(A #= 3) or (B #= 3),
((A #> 3 or B #>3) ->
    write("expression 1")
;
    write("expression 2")
).

由于AB的值不确定,条件总是为真,会打印expression 1的值。另外,结果是:

A = A{1 .. 10}
B = B{1 .. 10}
There are 6 delayed goals.
Yes (0.00s cpu)

如您所见,AB 的边界不会改变,因为它暂停到未来的表达式,而我们没有,边界不会改变。

现在试试不同的例子:

?- [A,B] #:: 1..10,
(A #= 3) or (B #= 3),
((A #> 3 or B #>3) ->
    write("expression 1")
;
    write("expression 2")
),
A = 3. % this line is added

由于A的值被确定A #> 3是不正确的,那么B呢?那是等于 3 还是大于 3?正如我们所说,条件部分将被执行!因此,B 的最后一个约束是 B #> 3。而除了write("expression 1")的执行,结果是:

A = 3
B = B{4 .. 10}
Yes (0.00s cpu)

最后一个例子是:

?- [A,B] #:: 1..10,
(A #= 3) or (B #= 3),
((A #> 3 or B #>3) ->
    write("expression 1")
;
    write("expression 2")
),
A = 3,
B = 3. % this line is added

同样在这个例子中打印了expression 1,结果是:

No (0.00s cpu)

是因为箭头总是expression 1会被执行。

一个解决方案 正在使用 ; 运算符喜欢以下内容:

[A,B] #:: 1..10, 
(
    (A = 3, B = 3, write('expression 21'))
    ; 
    (A = 3, B #> 3, write('expression 11'))
    ; 
    (A #> 3, B #> 3, write('expression 12'))
    ; 
    (A #> 3, B = 3, write('expression 13'))
), 
A = 3,
B = 5.

以上代码的结果是:

A = 3
B = 5
Yes (0.00s cpu, solution 1, maybe more)

并打印:

expression 21 expression 11

表示第一个分支down了,但是失败了自动回溯,进入下一个case!当应用下一个案例时,一切都成功了!

只要您坚持纯逻辑,约束编程就非常适合 Prolog。但是,正如您所展示的那样,不能自由地将 cut (!)if-then-else (->;) 等程序元素与约束逻辑。

仅当条件在 "constraint setup time" 处包含(即无条件为真)或不包含(无条件为假)时,使用 if-then-else 或削减才是安全的。实际上,这意味着此类条件不应包含 问题变量 (域变量等),而仅包含先验已知的 问题参数 (常量) .专用建模语言区分了这两件事,但 Prolog 没有。

如何在约束模型中不表达备选方案

上面的意思是你不能用cut/if-then-else来表达你想表达的那种选择。简单地摆脱条件的 committed-choice 方面并重写为纯析取可能很诱人。例如,您可以重写

( Usage #>= 1000 -> Rate#=7, Bonus#=100              % WRONG
;                   Rate#=9, Bonus#=0
)

作为纯析取

( Usage #>= 1000, Rate#=7, Bonus#=100                % INEFFICIENT
; Usage #<  1000, Rate#=9, Bonus#=0
)

虽然现在这在逻辑上是正确的,但不要这样做! Prolog 通过回溯探索备选方案(使用分号 (;) 或多个子句表示),即首先急切地选择一个备选方案,然后再返回另一个备选方案。这通常会毁掉任何有效约束程序的希望!在约束程序中,所有搜索都应位于 search/labeling 例程中。

具体化约束

如果条件和备选方案的分支都是具有具体实现的约束(即可以在布尔变量中反映约束真实性的实现),那么你很幸运:你可以重写整个借助具体化约束的特殊连接词的条件选择(在 ECLiPSe 中:andorneg=>#=)。对于上面的例子:

Usage #>= 1000  =>  Rate#=7 and Bonus#=100,            % OK
Usage #<  1000  =>  Rate#=9 and Bonus#=0

Usage #>= 1000 and Rate#=7 and Bonus#=100 or           % OK
Usage #<  1000 and Rate#=9 and Bonus#=0

很遗憾,只有基本的算术约束有具体化的版本,可以这样组合!

使用其他 built-in 约束

在某种程度上,处理备选方案是解决问题中最困难的部分,许多 built-in 约束解决了这个问题。因此,值得检查问题是否可以在现有 built-in 约束 之上建模,而 在模型中没有任何明确的分离。候选人是 element/3, table/2. disjunctive/2 和许多其他人。

延迟选择

最后的解决方案是推迟对备选方案的探索,直到可以明确确定条件的真实性。在 ECLiPSe 中,这是最简单的延迟子句。使用 OP 的示例:

delay choice(A, B) if var(A);var(B).     % wait for A,B to be known
choice(A, B) :-
    ( (A>3 ; B>3) ->                     % can now use normal Prolog tests
        write("expression 1")
    ;
        write("expression 2")
    ).

这有效,但只有在 A 和 B 都被实例化后才会起作用。如果在这种情况下,条件是可具体化的,我们可以做得更好:

choice(A, B) :-
    Bool #= (A#>3 or B#>3),
    delayed_choice(Bool).

delay delayed_choice(Bool) if var(Bool).
delayed_choice(1) :- write("expression 1").
delayed_choice(0) :- write("expression 2").

这将在域满足条件时执行:

?- choice(A, B), B #> 3.
expression 1

将一般析取转换为约束

ECLiPSe 有一个很棒的功能叫做 Generalised Propagation library(propia)。这可以通过使用简单的注释有效地将 Prolog 析取转换为约束。从上面正确但低效的公式开始,我们可以写成:

?- ( Usage #>= 1000, Rate#=7, Bonus#=100
   ; Usage #<  1000, Rate#=9, Bonus#=0
   ) infers most.

Usage = Usage{-1.0Inf .. 1.0Inf}
Rate = Rate{[7, 9]}
Bonus = Bonus{[0, 100]}
There is 1 delayed goal.
Yes (0.00s cpu)

正如 RateBonus 的域所示,有用的信息已经从析取中提取出来,甚至在决定适用的替代方案之前。