【问题标题】:Understanding CLP(FD) Prolog code of N-queens problem理解 N-queens 问题的 CLP(FD) Prolog 代码
【发布时间】:2018-11-21 06:28:22
【问题描述】:

我正在尝试理解 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).

我无法理解下面的sn-p:

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).

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

【问题讨论】:

  • 如果我明白你在问什么,代码可以工作,但你不明白?
  • 嗨 Enigmativity,感谢您的快速回复。是的,代码可以工作,但我不明白。
  • 感兴趣的:一个更简单的版本,不是我们clpfd,但只能解决标准的 8x8 板NQUEENS
  • 谢谢你,我会调查的。
  • 我想我们可以点击星号使其成为收藏夹。这是我的第一个问题,所以没有那么多想法。谢谢。

标签: prolog clpfd n-queens


【解决方案1】:

由于您没有提供任何示例查询,因此请从一些示例查询开始确定参数和输出格式。 通常,要确定未知代码的参数和输出格式,需要查看参数结构的代码,然后尝试示例查询。另外请注意,此代码使用Constraint Logic Programmingclpfd;当我读到我真的停止思考syntactic unification 并开始思考constraints。我认为它是嵌入在 Prolog 中的一个单独的系统,而不是额外的谓词。您会注意到,在这个答案中,constraint 经常被使用,而 predicaterule 却很少出现,即使这是 Prolog。

由于 N-Queens 问题是众所周知的逻辑问题,因此快速 Google 搜索 (clpfd n queens) 会出现 SWI-Prolog Example: Eight queens puzzle。注意添加了关键字clpfd,这对于理解代码的这种变体至关重要;其他编程语言有many解决方案。

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

Eight queens puzzle 的 Wikipedia 文章在 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] 
...

现在我们知道代码运行并给出了有效的解决方案,我们可以开始剖析它。
通常以gtrace/0 开头的SWI-Prolog trace/0 或SWI-PROlog GUI-tracer 将是首选工具,但在我知道这不是Constraint Logic Programming 的首选工具之前在clpfd 上使用过它。试试看,你就会明白为什么。

开始解剖。

?- 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) 运算符,它们通常是约束,例如#\=#= 像普通运算符一样被读取,而不是 #

`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 棋盘和白色皇后作为有效位置,黑色皇后作为约束所定义的无效位置。

A1,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.

如果使用queen A和queen 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 的列表也是一个约束,(@987654436 @) 并将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 才解决约束以生成结果。

【讨论】:

  • 很棒的答案?如果您在阅读此答案时学到了任何东西,请记住欢迎投票。通常我不会提到这个,但这个答案花了 4 个小时。
  • 伙计,你是一颗宝石。我在互联网上的任何地方都没有看到这么多的详细说明。作为我自己在人工智能和序言部分的天真,我得到的比我预期的要多得多。你有我的全部尊重。我对答案投了赞成票,但不知何故,stackoverflow 把它放在了观察中(不公开显示),因为这是我在这里的第一个问题。我会再试一次。感谢您的精彩阐述
猜你喜欢
  • 2019-08-30
  • 2022-06-10
  • 1970-01-01
  • 1970-01-01
  • 2023-03-12
  • 1970-01-01
  • 1970-01-01
  • 2020-09-02
  • 2013-03-28
相关资源
最近更新 更多