N皇后问题是个经典问题,在 N*N 的国际象棋棋盘内放下N个皇后有几种解法,使其不能互相攻击,属于典型的回溯问题。国际象棋中的皇后可以横、竖、斜走,很强大。
(好久没下了)
2x2 和 3x3 棋盘肯定不行,斜线都会碰到;从 4*4 棋盘开始有解,并且增速很快。
为了方便解释,直接用二维数组表示。1表示放置的位置,空和0表示没放,x表示检查过不行(也是0)。代码中 i 表示行,j 表示列。
算法核心两个函数即可:place 用于检查目标位置 [i, j] 是否能放,即同一行、同一列、斜线(包括四个方向)有没有1,直接用粗暴方法就好。queen 用于递归循环和回溯,从 [0, 0] 开始放皇后,然后找第1行哪个位置能放,如果第1行能放再找第2行哪里能放,当 i == 边长(注意数从0开始)就是找到了解法。每一行找到能放的地方后递归进入下一行,递归完后进行回溯,寻找本行能否有另一个可放置位置。
流程演示:
在 [0, 0] 放下
找第1行哪里能放,找到 [1, 2];找第2行哪里能放,没有位置回退
第1行放 [1, 2] 不行了,回溯后标记为0;在 [1, 3] 也可以放下,然后找第2行找到 [2, 1] 可以放,但是第3行没地方放,回退
4x4 棋盘放在 [0, 0] 不行,就在 [0, 1] 放第一个,慢慢检查,找到的第一个解
其实难点不在检查在回溯不丢失可能解,当问题数据大时因为递归复杂度很高。我试了下我的 MBP 15 (Mid 2014) 跑8/9个卡一下就出来,但是10个就要时间了。
# coding=utf-8
"""
Python “没有数组”只有广义表
二维“数组”格式 [[], [], []]
for 两变量循环时要用 zip 把俩循环范围包装起来
"""
def place(i, j, board):
lent = len(board)
# 行,直接检查每个 list 有几个1
if board[i].count(1) != 0:
return False
# 列,每个 list 第j号元素是不是1
for n in range(lent):
if board[n][j] == 1:
return False
# 左上
for n1, n2 in zip(range(i-1, -1, -1), range(j-1, -1, -1)):
if board[n1][n2] == 1:
return False
# 右下
for n1, n2 in zip(range(i+1, lent), range(j+1, lent)):
if board[n1][n2] == 1:
return False
# 右上
for n1, n2 in zip(range(i-1, -1, -1), range(j+1, lent)):
if board[n1][n2] == 1:
return False
# 左下
for n1, n2 in zip(range(i+1, lent), range(j-1, -1, -1)):
if board[n1][n2] == 1:
return False
return True
def queen(i, board):
if i == len(board):
print(board)
return
# 找每一行第几列能放
for j in range(len(board)):
if place(i, j, board):
board[i][j] = 1
# 进入递归找下一行可放位置
queen(i+1, board)
# 递归完后回溯
board[i][j] = 0
n = int(input("皇后个数?"))
board = []
# 初始化二维数组
for n1 in range(n):
board.append([])
for n2 in range(n):
board[n1].append(0)
queen(0, board)
#include
#include
#include
void printBoard(bool* board, short a);
bool check(bool* board, short i, short j, short a);
void queen(short i, bool* board, short a);
int main(int argc, const char * argv[])
{
int a;
printf("皇后个数?");
scanf("%d", &a);
bool board[a][a];
for (short i1 = 0; i1 < a; i1++)
for (short i2 = 0; i2 < a; i2++)
board[i1][i2] = 0;
queen(0, (bool*) board, a);
return 0;
}
void queen(short i, bool* board, short a)
{
if (i == a)
{
printBoard(board, a);
return;
}
for (short j = 0; j < a; j++)
{
if (check(board, i, j, a))
{
*(board+i*a+j) = 1;
queen(i+1, board, a);
*(board+i*a+j) = 0;
}
}
}
void printBoard(bool* board, short a)
{
for (uint8_t i1 = 0; i1 < a; i1++)
{
for (short i2 = 0; i2 < a; i2++)
printf("%d", *(board+i1*a+i2));
puts("");
}
puts("");
}
bool check(bool* board, short i, short j, short a)
{
// 行
for (short n = 0; n < a; n++)
if (*(board+i*a+n) == 1)
return false;
// 列
for (short n = 0; n < a; n++)
if (*(board+n*a+j) == 1)
return false;
// 左上
for (short n1 = i-1, n2 = j-1; n1 >= 0 && n2 >= 0; n1--, n2--)
if (*(board+a*n1+n2) == 1)
return false;
// 左下
for (short n1 = i-1, n2 = j+1; n1 >= 0 && n2 < a; n1--, n2++)
if (*(board+a*n1+n2) == 1)
return false;
// 右下
for (short n1 = i+1, n2 = j+1; n1 < a && n2 < a; n1++, n2++)
if (*(board+a*n1+n2) == 1)
return false;
// 右上
for (short n1 = i+1, n2 = j-1; n1 < a && n2 >= 0; n1++, n2--)
if (*(board+a*n1+n2) == 1)
return false;
return true;
}
和 Python 的一样,就是语言变了,处理二维数组比较难受。稍微改了一下输出,变成密恐福音。C++ 和 C 写法类似,就是有高级些功能比如用 vector 和 cout,推荐 C++ 不要混用 C 写法。
因为每一行只有一个位置能放,可以只用一维数组存储,数组第n个元素的值代表第n行哪一列能放。
检查函数也可以简化,稍微需要一些数学逻辑。设被检查位置相当于二维数组 [row, col],同列的情况即一维数组存在与 col 相同的值,对角线冲突情况则是行之差和列之差绝对值相等。
比如下面:真实数组里是 [?, 1, 2, 0],行之差为1,列之差为1,冲突。
再看一种例子:真实数组里是 [3, 2, ?, ?],行之差为 -1,列之差为 1,也是冲突的。
C/C++ 的就懒得写了。
def check(board, row, col):
for i in range(row):
if board[i]-col == 0 or abs(board[i]-col) == abs(i-row):
return False
return True
def queen(board, row):
if row >= len(board):
print(board)
for col in range(len(board)):
if check(board, row, col):
board[row] = col
queen(board, row+1)
n = int(input("皇后数量?"))
board = [0 for i in range(n)]
queen(board, 0)