理解 N 皇后问题的 CLP(FD) Prolog 代码

Understanding CLP(FD) Prolog code of N-queens problem

我正在尝试理解 N 皇后问题的解决方案,如下所示:

:- use_module(library(clpfd)).

n_queens(N, Qs) :-
    length(Qs, N),
    Qs ins 1..N,
    safe_queens(Qs).

safe_queens([]).
safe_queens([Q|Qs]) :-
    safe_queens(Qs, Q, 1),
    safe_queens(Qs).

safe_queens([], _, _).
safe_queens([Q|Qs], Q0, D0) :-
    Q0 #\= Q,
    abs(Q0 - Q) #\= D0,
    D1 #= D0 + 1,
    safe_queens(Qs, Q0, D1).

我无法理解以下代码段:

safe_queens([]).
safe_queens([Q|Qs]) :-
    safe_queens(Qs, Q, 1),
    safe_queens(Qs).

safe_queens([], _, _).
safe_queens([Q|Qs], Q0, D0) :-
    Q0 #\= Q,
    abs(Q0 - Q) #\= D0,
    D1 #= D0 + 1,
    safe_queens(Qs, Q0, D1).

请帮助我理解。任何帮助将不胜感激。

由于您没有给出任何示例查询,所以先从一些示例查询开始,以确定参数和输出格式。 通常,要确定未知代码的参数和输出格式,需要查看参数结构的代码,然后尝试示例查询。另外请注意,此代码使用 Constraint Logic Programming library clpfd; when I read that I literally stop thinking syntactic unification and start thinking constraints。我认为它是嵌入在 Prolog 中的独立系统,而不是额外的谓词。你会注意到在这个答案中经常使用 constraintpredicaterule 很少见,即使这是 Prolog。

由于 N 皇后问题作为一个逻辑问题而广为人知,因此可以快速 Google 搜索(clpfd n queens) turns up SWI-Prolog Example: Eight queens puzzle. Note the addition of the keyword clpfd it is crucial for understanding this variation of the code; there are many 其他编程语言的解决方案。

这给出了一个示例查询 n_queens(8, Qs), label(Qs),系统生成的变量的 label/1 returns 值。 这也告诉我们第一个参数是一个正整数,第二个参数是第一个参数长度的列表。 同样通过之前处理过这个问题,第一个参数是电路板的尺寸,所以 11x1 电路板,88x8 电路板,等等。 ,以及棋盘上的皇后数。
接下来有帮助的是了解有效的解决方案是什么,或者至少知道一组参数的有效解决方案的数量。

Eight queens puzzle provides that in the counting solutions 部分的维基百科文章。 这表明对于 1x1 的板有一个解决方案,对于 2x2 或 3x3 的板没有解决方案,对于 4x4 有两个解决方案等等。

对于 1x1 板有一个解决方案。

?- n_queens(1,Qs),label(Qs).
Qs = [1].

对于 2x2 板没有解决方案。

?- n_queens(2,Qs),label(Qs).
false.

对于 4x4 板有两种解决方案。

?- n_queens(4,Qs),label(Qs).
Qs = [2, 4, 1, 3] ;
Qs = [3, 1, 4, 2] ;
false.


Qs = [2, 4, 1, 3]

为了解释结果,列表中的位置对应于板上的列和板上一行的值,因此对于列表中的第一个值 (2),它显示为 a queen in row 2, column 1,对于列表中的第二个值 (4),它显示为 a queen in row 4, column 2

Qs = [3, 1, 4, 2]

注意:使用Chess Diagram Setup

生成的图像

如果我们 运行 将值作为变量进行查询,结果将是无穷无尽的有效答案。

?- n_queens(N,Qs),label(Qs).
N = 0,
Qs = [] ;
N = 1,
Qs = [1] ;
N = 4,
Qs = [2, 4, 1, 3] ;
N = 4,
Qs = [3, 1, 4, 2] ;
N = 5,
Qs = [1, 3, 5, 2, 4] ;
N = 5,
Qs = [1, 4, 2, 5, 3] ;
N = 5,
Qs = [2, 4, 1, 3, 5] ;
N = 5,
Qs = [2, 5, 3, 1, 4] ;
N = 5,
Qs = [3, 1, 4, 2, 5] ;
N = 5,
Qs = [3, 5, 2, 4, 1] ;
N = 5,
Qs = [4, 1, 3, 5, 2] 
...

既然我们知道了代码 运行s 并给出了有效的解决方案,我们就可以开始剖析它了。
通常是 SWI-Prolog trace/0 or SWI-PRolog GUI-tracer started with gtrace/0 would be a tool of choice but having used that on clpfd before I know that is not a tool of first choice with Constraint Logic Programming。试一试,你就会明白为什么。

继续解剖。

?- n_queens(1,Qs).
Qs = [1].

?- n_queens(2,Qs).
Qs = [_1942, _1948],
_1942 in 1..2,
abs(_1942-_1948)#\=1,
_1942#\=_1948,
_1948 in 1..2.

这很有趣。
为了使这更容易理解,将系统生成的变量替换为用户友好的变量,并让人们阅读语句的含义。

?- n_queens(2,Qs).
Qs = [A, B],
A in 1..2,
abs(A-B)#\=1,
A#\=B,
B in 1..2.

请注意,其中带有 # 的 CLP(FD) 运算符通常是约束,例如#\= and #= 像普通运算符一样读取,少了 #

`A in 1..2`    reads the value for `A` must be in the range `1..2`
`abs(A-B)#\=1` reads the difference of the values between `A` and `B` must not equal 1
`A#\=B`        reads the value of `A` must not equal the value of `B`
`B in 1..2`    reads the value of `B` must be in `1..2`

所以这些只是一组限制条件。如果您尝试手动解决约束,您会发现没有解决方案,例如

0,_  invalid by `A in 1..2`
_,0  invalid by `B in 1..2`
3,_  invalid by `A in 1..2`
_,3  invalid by `B in 1..2`
1,1  invalid by `A#\=B` 
1,2  invalid by `abs(A-B)#\=1`
2,1  invalid by `abs(A-B)#\=1`
2,2  invalid by `A#\=B` 

对 4x4 板做同样的事情

?- n_queens(4,Qs).
Qs = [_5398, _5404, _5410, _5416],
_5398 in 1..4,
abs(_5398-_5416)#\=3,
_5398#\=_5416,
abs(_5398-_5410)#\=2,
_5398#\=_5410,
abs(_5398-_5404)#\=1,
_5398#\=_5404,
_5416 in 1..4,
abs(_5410-_5416)#\=1,
_5410#\=_5416,
abs(_5404-_5416)#\=2,
_5404#\=_5416,
_5410 in 1..4,
abs(_5404-_5410)#\=1,
_5404#\=_5410,
_5404 in 1..4.


?- n_queens(4,Qs).
Qs = [A, B, C, D],
A in 1..4,     reads the value for `A` must be in the range `1..4`
abs(A-D)#\=3,  reads the difference of the values between `A` and `D` must not equal 3
A#\=D,         reads the value of `A` must not equal the value of `D`
abs(A-C)#\=2,  reads the difference of the values between `A` and `C` must not equal 2
A#\=C,         reads the value of `A` must not equal the value of `C`
abs(A-B)#\=1,  reads the difference of the values between `A` and `B` must not equal 1
A#\=B,         reads the value of `A` must not equal the value of `B`
D in 1..4,     reads the value for `D` must be in the range `1..4`
abs(C-D)#\=1,  reads the difference of the values between `C` and `D` must not equal 1
C#\=D,         reads the value of `C` must not equal the value of `D`
abs(B-D)#\=2,  reads the difference of the values between `B` and `D` must not equal 2
B#\=D,         reads the value of `B` must not equal the value of `D`
C in 1..4,     reads the value for `C` must be in the range `1..4`
abs(B-C)#\=1,  reads the difference of the values between `B` and `C` must not equal 1
B#\=C,         reads the value of `B` must not equal the value of `C`
B in 1..4.     reads the value for `B` must be in the range `1..4`

这有点难以接受,但这是逻辑,我们可以重新排列语句,含义将是相同的。

所以对类似的语句进行分组,按变量排序,然后按简单性对组进行排序给出

`A in 1..4`    reads the value for `A` must be in the range `1..4`
`B in 1..4`    reads the value for `B` must be in the range `1..4`   
`D in 1..4`    reads the value for `D` must be in the range `1..4`
`C in 1..4`    reads the value for `C` must be in the range `1..4` 
`A#\=B`        reads the value of `A` must not equal the value of `B`
`A#\=C`        reads the value of `A` must not equal the value of `C`
`A#\=D`        reads the value of `A` must not equal the value of `D`
`B#\=C`        reads the value of `B` must not equal the value of `C`
`B#\=D`        reads the value of `B` must not equal the value of `D`
`C#\=D`        reads the value of `C` must not equal the value of `D`
`abs(A-B)#\=1` reads the difference of the values between `A` and `B` must not equal 1
`abs(A-C)#\=2` reads the difference of the values between `A` and `C` must not equal 2
`abs(A-D)#\=3` reads the difference of the values between `A` and `D` must not equal 3
`abs(B-C)#\=1` reads the difference of the values between `B` and `C` must not equal 1
`abs(B-D)#\=2` reads the difference of the values between `B` and `D` must not equal 2
`abs(C-D)#\=1` reads the difference of the values between `C` and `D` must not equal 1

现在解释约束并展示它们与方板上皇后的关系;请注意,我说的是方形棋盘而不是棋盘,因为棋盘是 8x8 的,并且此代码适用于不同尺寸的方形棋盘。


A in 1..4

意味着 A 皇后必须放在 4x4 棋盘上的某个位置。在处理约束问题时,您经常会发现我们作为人类认为理所当然或认为常识需要作为特定约束给出,这就是一个案例。还要了解为常识添加规则有时是创建 AI 解决方案时最艰巨的任务之一。虽然我找不到参考,但当 Cyc 的创建者添加规则时,时间的概念花了很多时间才正确(无双关语)。 A in 1..4 等其他约束只是确保没有皇后被放置在棋盘外的位置。


A#\=B

为了更好地理解这个约束,让我们做一张图片,其中 4x4 棋盘和白皇后作为约束定义的有效位置,黑皇后作为无效位置。

所以 A 是第 1 行的白皇后,B 是第 1 行的黑皇后。由于 A 不能等于 B 这表示如果皇后 A 在第 1 行然后皇后 B 不能在第 1 行。由于规则与变量一起使用,这意味着对于任何行,A 皇后在 B 皇后不能在那排。 A#\=B 等约束的其余部分只是确保没有两个皇后可以在同一行。

将此约束视为女王的水平攻击。


abs(A-B)#\=1

为了更好地理解这个约束,让我们做一张图片,其中 4x4 棋盘和白皇后作为约束定义的有效位置,黑皇后作为无效位置。

A 1,2,3,4 有四个位置但是因为规则是水平对称的(1 和 4 一样,2 和 3 一样)我只会做其中两个.

A为1时

因为A是1,所以B不可能是2。

1-2 = -1
ABS(-1) = 1
1 can not equal 1.

A为2时

因为A是2,所以B不可能是1。

2 - 1 = 1
ABS(1) = 1
1 can not equal 1.

因为A是2,所以B不可能是3。

2 - 3 = -1
ABS(-1) = 1
1 can not equal 1.

如果检查使用皇后 A 和皇后 D 的约束

abs(A-D)#\=3

A为1时

因为A是1,所以D不可能是4。

1-4 = -3
ABS(-3) = 3
3 can not equal 1.

A为2时

因为A是2,所以D可以是1

2-1 = 1
ABS(1) = 1
1 can not equal 3.

因为A是2,所以D可以是2

2-2 = 0
ABS(0) = 0
0 can not equal 3.

因为A是2,所以D可以是3

2-3 = -1
ABS(-1) = 1
1 can not equal 3.

因为A是2,所以D可以是4

2-4 = -2
ABS(-2) = 2
2 can not equal 3.

将此约束视为女王的对角线攻击。


但是等一下,女王可以水平,垂直和对角线移动,垂直移动的约束在哪里?

虽然这在示例查询的输出中并未显示为约束,但存在约束。到目前为止,我们有限制皇后在棋盘上的位置的约束,水平攻击和对角线攻击作为不同的约束,但是数据的结构,长度为 N 的列表也是一个约束,([A,B,C,D]) 并将 A 皇后限制在第一列,B 皇后在第二列,依此类推。同样,学习 AI 编码的要点之一是,我们作为人类的思考方式并不总是直接转化为如何使用计算机解决问题。因此,这段代码在使用约束来解决问题的同时,还使用了数据结构。

将列表视为女王的专栏攻击。

同一列中不能有两个皇后,这是受限于标量变量中不能有两个值。


在这一点上,你们中的许多人会认识到代码的其余部分是辅助和递归谓词 safe_queens/1 以及递归谓词 safe_queens/3.


safe_queens([], _, _).
safe_queens([Q|Qs], Q0, D0) :-
    Q0 #\= Q,
    abs(Q0 - Q) #\= D0,
    D1 #= D0 + 1,
    safe_queens(Qs, Q0, D1).

这是处理列表的标准递归调用,例如

safe_queens([], _, _).
safe_queens([H|T], _, _) :-
    % Process head of list (H)
    safe_queens(T, _, _). % Process tail of list (T)

这两个语句

Q0 #\= Q
abs(Q0 - Q) #\= D0

上面有解释

D1 #= D0 + 1

D1 设置为 D0 + 1

如果我们这样修改谓词

permutations([], _, _).
permutations([Q|Qs], Q0, D0) :-
    write(Q0),write('#\='),writeln(Q),
    write('abs('),write(Q0),write('-'),write(Q),write(')#\='),writeln(D0),
    D1 is D0 + 1,
    permutations(Qs, Q0, D1).

和运行这些查询我们看到它生成了一些约束

?- permutations(['B','C','D'],'A',1).
A#\=B
abs(A-B)#\=1
A#\=C
abs(A-C)#\=2
A#\=D
abs(A-D)#\=3
true.

?- permutations(['C','D'],'B',1).
B#\=C
abs(B-C)#\=1
B#\=D
abs(B-D)#\=2
true.

?- permutations(['D'],'C',1).
C#\=D
abs(C-D)#\=1
true.

safe_queens([]).
safe_queens([Q|Qs]) :-
    safe_queens(Qs, Q, 1),
    safe_queens(Qs).

这是处理列表的标准递归调用,例如

safe_queens([]).
safe_queens([H|T]) :-
    % Process head of list (H)
    safe_queens(T). % Process tail of list (T)

也是safe_queens/3的帮手,因为这个语句

    safe_queens(Qs, Q, 1)

safe_queens/3的第三个参数初始化为1

如果我们这样修改谓词

generate_args([]).
generate_args([Q|Qs]) :-
    write('Qs: '),write(Qs),write(', Q: '),write(Q),writeln(', 1'),
    generate_args(Qs).

和 运行 这个查询我们看到它生成了 safe_queens/3

所需的参数
?- generate_args(['A','B','C','D']).
Qs: [B,C,D], Q: A, 1
Qs: [C,D], Q: B, 1
Qs: [D], Q: C, 1
Qs: [], Q: D, 1
true.

但是在你的问题中你没有问第一个谓词

n_queens(N, Qs) :-
    length(Qs, N),
    Qs ins 1..N,
    safe_queens(Qs).

其中有

length(Qs,N)

生成长度为N的未绑定变量列表

[A,B,C,D]

并且有关键约束语句

Qs ins 1..N

生成类似

的约束
A in 1..4


现在附加到查询的关键区别

labels(Qs)

如果您使用 SWI-Prolog GUI-tracer 和 运行 直到 n_queens/2 末尾的代码,您将在调试器中看到约束列表而不是解决方案

那是因为那些谓词生成了内部维护的约束,直到调用了labels/1,约束才被求解产生结果。