问题:在一个8*8大小的国际象棋棋盘上放置8个皇后棋子,使得所有的皇后都是安全的(即任意两个皇后都无法攻击到对方)。
分析:
按照国际象棋的规则,皇后的攻击方式是横,竖和斜向。
皇后可以攻击到同一列所有其它棋子,因此可推导出每1列只能存在1个皇后,即每个皇后分别占据一列。棋盘一共8列,刚好放置8个皇后。
为了摆放出满足条件的8个皇后的布局,可以按如下方式逐步操作:
1. 在第0列找到一个位置放置第1个皇后;
2. 在第1列找到一个位置放置第2个皇后;
3. 在第2列找到一个位置放置第3个皇后;
4. 对第3,4,5,6,7,8列都执行放置操作;
5. 当执行完“在第7列找到一个放置第8个皇后”这一操作完毕后,问题求解完毕。
观察1,2,3,4 这些操作步骤,可以发现是一个重复的动作,抽象一下,即“在第 n 列放下第 n+1 个皇后”,n 从0到7。看起来我们需要写一个这样的过程:
func put(col int)
显然,需要使得 col 从0到7每次+1,一共执行8次 put。
可以考虑用循环或着递归来完成它,由于一般来说,使用递归解决问题的思路会更加自然和简单,因此我们选择使用递归。那么 put 看起来可能是这样的:
func put(col int) {
// 执行某些操作
put(cor+1)
}
我们猜测递归的启动方式可能是从 col = 0 开始,那么调用 put(0)
作为程序入口的 main 函数看起来是这样的:
func main() {
put(0)
}
接下来我们需要考虑递归如何结束。分析步骤5可知,当 col==8 时递归结束。因此我们继续写 put 的代码。
func put(col int) {
if col == 8 {
return
}
// 执行某些操作
put(col+1)
}
递归程序的框架看起来已经搭好了,接下来我们要求解的子问题是:
在第 col 列,找到一个位置,放下第 col+1 个皇后。
这个子问题的约束条件是:
放下皇后之后,棋盘上没有任意两个皇后可以攻击到对方,即我们找到的这个位置是安全的。
由于每次放下皇后时,只有8个位置可选,因此在某行放下皇后的操作步骤如下:
1. 在第0行放下皇后;
2. 检测当前位置是否安全;
3. 如果安全,则子问题求解结束;
4. 如果不安全,则在下一个位置放皇后;
5. 回到步骤3,继续。
看起来这是一个循环问题,写下代码:
for pos = 0; pos < 8; pos++ {
// 在 pos 处放下皇后
if safe() {
// 子问题求解结束
}
}
第3步找到安全位置后,我们就去后一列找放置下一个皇后的位置了,即接下来调用 put(col+1),代码如下:
for pos := 0; pos < 8; pos++ {
// 在 pos 处放下皇后
if safe() {
put(col+1)
}
}
现在看一下目前的代码。
func put(col int) {
if col == 8 {
return
}
for pos := 0; pos < 8; pos++ {
// 在 pos 处放下皇后
if safe() {
put(col+1)
}
}
}
查看代码之后我们发现,代码里没有任何东西来表示棋盘,以及皇后的位置。看起来我们需要做点什么。
我们需要选用某个数据结构来表示棋盘和皇后的位置。
棋盘是由64个格子组成的,排列布局为8*8。第一反应是可以用一个二维数组来表示,下标是格子的坐标,数组元素选用 bool 型, true 表示此处放置了皇后, false 表示空格子。
初始时,数组中的64个元素都为 false, 程序运行之后,其中8个元素的值变为 true。
但看起来似乎有点浪费,而且处理多维数组的下标比较麻烦。我们可以再想想,是否可以简化一下棋盘的表示。
让我们再用自然语言描述一下最终求出的解,也许是这样的:
第0列的第a行有皇后
第1列的第b行有皇后
第2列的第c行有皇后
...
第7列的第h行有皇后
一维数组 [a, b, c, d, e, f, g, h] 就能表示上面这个解的全部信息。不妨先用简单的试试看。
对于 go 语言,slice 是比数组好得多的选择,我们用一个类型为 [8]int 的变量 boards 来表示棋盘。现在代码如下:
func put(col int) {
if col == 8 {
fmt.Println(boards) // 输出答案
return
}
for pos := 0; pos < 8; pos++ {
boards[col] = pos // 在 pos 处放下皇后
if safe() {
put(col+1)
}
}
}
出于遵循“变量的作用域尽可能小”这一设计原则,我们把 boards 这个类型为[8]int 的 slice 作为 put 的参数。现在代码如下:
func put(boards [8]int, col int) {
if col == 8 {
fmt.Println(boards) // 输出答案
return
}
for pos := 0; pos < 8; pos++ {
boards[col] = pos // 在 pos 处放下皇后
if safe() {
put(boards, col+1)
}
}
}
初始时,我们需要一个空的棋盘。在 main 函数再加点东西。
func main() {
boards := [8]int{}
put(boards, 0)
}
有了可以表示棋盘的数据对象 boards 之后,我们来完成 safe 这个函数。
boards 作为 safe 的参数,接下来我们要检验棋盘上 pos 这个位置是否安全。
咋一看,我们可能想到遍历整个棋盘,看看是否有任意两个皇后可以攻击。但我们再阅读一下已经写好的代码并仔细回顾最初的解题操作步骤,代码显示我们的操作逻辑是:
1. 在第n行的 pos (pos从0到7) 放下皇后
2. 检测 pos 是否安全
3. ...
我们再看看“外”一层的操作步骤:
1. 在第0列找到一个位置放置第1个皇后;
2. 在第1列找到一个位置放置第2个皇后;
3. ...
4. 在第6列找到一个位置方式第7个皇后;
5. ...
我们发现,按照这个操作步骤,当我们在第 n 列寻找安全格子 pos 时,我们只需要检查这个 pos 是否会被前面列的那些皇后们攻击到,而这些皇后们本身彼此都是不会互相攻击。
那么检查的操作步骤如下:
1. 获取第0列皇后的位置;
2. 检查是否会攻击到第n列的 pos;
3. 如果能攻击到,则不安全;
4. 如果安全,则循环计数加1,回到步骤1继续;
5. ...
6. 循环的的最后一次时检查第 n-1 列皇后是否会攻击到第n列的 pos,如果依然攻击不到,那么我们能确认 pos 是安全的。
因此,safe 函数的实现如下:
func safe(boards [8]int, col int, pos int) bool {
for c := 0; c < col; c++ {
if isAttack(boards, c, col, pos) {
return false
}
}
return true
}
需要传递 boards , col 和 pos 三个参数。返回 bool 值,false 表示不安全, true 表示安全。
代码反映了一个事实:只要有一个皇后能攻击到 pos ,则 pos 就是不安全的,只有检车了前面 n-1 个皇后,它们全都不能攻击 pos ,我们才可以说 pos 是安全的。
现在 put 的代码如下:
func put(boards [8]int, col int) {
if col == 8 {
fmt.Println(boards) // 输出答案
return
}
for pos := 0; pos < 8; pos++ {
boards[col] = pos // 在 pos 处放下皇后
if safe(boards, col, pos) {
put(boards, col+1)
}
}
}
接下来我们着手实现 isAttack。
func isAttack(boards []int, c int, col int) bool {
switch {
case boards[c] == boards[col]:
return true
case boards[col]-boards[c] == c-col:
return true
case boards[col]-boards[c] == col-c:
return true
}
return false
}
这个 function 需要4个参数让我们嗅到了一点不好的味道,其中有3个参数都是从 safe 延续而来的,我们来看看调用 safe 的代码片段:
...
boards[col] = pos // 在 pos 处放下皇后
if safe(boards, col, pos) {
...
}
boards, col, pos 是存在关联的,在执行完 boards[col] = pos 之后,boards[col] 的值就等于 pos 了,因此我们可以去掉 pos 这个参数,用 boards[col] 替换它。
最后再检查一遍,代码里多处出现8这个 magic number,这显然不好。
8是棋盘的尺寸,即 boards 这个 slice 的长度,我们动手修改 boards 的类型和初始化方式。
新的初始化代码如下:
size := 8
boards := make([]int, size)
现在,magic number 可以用 len(boards) 来替代了。
我们意外地发现,这次重构后,程序可以求解任意尺寸棋盘的皇后问题了。
最终完整代码如下:
package main
import "fmt"
func main() {
queen(8)
}
func queen(size int) {
boards := make([]int, size)
put(boards, 0)
}
func put(boards []int, col int) {
size := len(boards)
if col == size {
fmt.Println(boards) // 输出答案
return
}
for pos := 0; pos < size; pos++ {
boards[col] = pos // 在 pos 处放下皇后
if safe(boards, col) {
put(boards, col+1)
}
}
}
func safe(boards []int, col int) bool {
for c := 0; c < col; c++ {
if isAttack(boards, c, col) {
return false
}
}
return true
}
func isAttack(boards []int, c int, col int) bool {
switch {
case boards[c] == boards[col]:
return true
case boards[col]-boards[c] == c-col:
return true
case boards[col]-boards[c] == col-c:
return true
}
return false
}