Python实现八皇后问题:递归与非递归算法详解
本文深入解析八皇后问题的Python实现,涵盖从基础递归回溯到优化的一维数组表示,再到非递归栈结构的完整思路。通过对比二维与一维数据结构,分析算法复杂度,并提供获取单一解与所有92种布局的代码示例,同时附带C语言版本实现,帮助开发者掌握经典回溯算法的核心技巧。
八皇后问题描述
问题背景:国际象棋棋盘规格为8×8方格,每个格子可放置一枚棋子。皇后的攻击范围覆盖其所在行、列及两条对角线(左上、左下、右上、右下四个方向)。目标是在棋盘上放置8个皇后,使得任意两个皇后互不攻击(即两两不同行、不同列、不同对角线),求解一种或所有可能的布局方案。
解决该问题通常采用递归与非递归两种算法思路。首先介绍基于递归的算法实现。
解题逻辑自然地从行维度切入。由于8个皇后必须位于不同行,每行恰好有一个皇后,因此核心问题转化为确定每行皇后的列位置。同时需确保所有皇后的列下标互不相同,且任意两个皇后不在同一条对角线上。
首要任务是建立数学模型来描述对角线上的冲突。对于位于坐标(i,j)的皇后,其四个对角线方向上的格子坐标可表示为(i-n, j+n)、(i-n, j-n)、(i+n, j-n)和(i+n, j+n),其中n为步长,i±n和j±n需保持在[0,7]的有效范围内。忽略边界限制,判断两个格子(a,b)与(i,j)是否在同一条对角线上的充要条件是:|a - i| == |b - j|。
基于递归思想,我们从第一行开始逐行确定皇后的位置。对于当前行的每个可能位置(即与之前已放置的皇后无冲突的位置),进行递归深入。若某行所有可能位置均导致冲突(即可能位置数为0),则判定为死路,回溯至上一层递归尝试其他分支;若递归到达第九行(实际不存在第九行,仅表示前八行已成功配置),则说明找到一个有效解决方案。
寻找单行内皇后合适位置的过程是一个递归子过程。进入该递归时,需传递两个关键参数:1. 当前棋盘的皇后放置状态;2. 当前处理的行号。
因此,递归主体函数可设计为EightQueen(board, row),其中board表示棋盘状态(例如二维数组,0代表空位,1代表有皇后)。此外,可定义辅助函数check(board, pos),其中pos为(x,y)元组,用于判断在现有board状态下,在pos位置放置新皇后是否会产生冲突。
基于上述思路,初步代码实现如下:
def check(board,pos):
# check函数暂时先不实现
pass
def EightQueen(board,row):
blen = len(board)
if row == blen: # 来到不存在的第九行了
print board
return True # 一定要return一个True,理由在下面
for possibleY in range(blen):
if check(board,(row,possibleY)):
board[row][possibleY] = 1 # 放置一个Queen
if not EightQueen(board,row+1): # 这里其实是本行下面所有行放置皇后的递归入口。但是如果最终这条路没有找到一个解,那么
# 此时应该将刚才放置的皇后收回,再去寻找下一个可能的解
board[row][possibleY] = 0
else:
return True
return False
初始设计中,递归终止条件可能仅返回而不带值,主循环中放置皇后后直接调用EightQueen且不做逻辑判断。但这会导致一个问题:当某层递归的for possibleY循环遍历完所有列却未找到合法解(即本行无合适位置)时,返回上一层,上一层的possibleY右移一格,但本行之前放置的皇后位置仍为1,导致后续所有check检查均失败。因此,需设计机制:若第一个possibleY未找到最终解(此处增加判断条件),在尝试下一个possibleY前,必须收回当前行的皇后。
该判断机制为:若某层递归的for possibleY循环全部遍历完毕仍未找到解,则返回False(EightQueen函数最终返回值);上层递归根据此False反馈,移除前一个皇后;若找到解,则一路返回True,结束函数运行。
若仅需获取一个解,可在if row == blen时打印board并直接调用sys.exit(0)。此时,for possibleY循环结束后只需返回False。主循环中仍需保留对递归返回值的判断:if not EightQueen。
上述代码未实现check函数。仔细分析,若按原设想实现check函数较为困难。例如令x,y=pos,虽只需检查行下标小于x的行,但需遍历每行找到值为1的格子(即one-hot编码模式),再与x,y进行冲突判断。这导致check函数复杂度可能达到O(n^2),嵌套外层循环后总复杂度急剧上升。以下是check函数的一种可能实现:
def check(board,pos):
x,y = pos
blen = len(board)
for i in range(x):
for j in range(blen):
if board[i][j] == 1:
if j == y or abs(j-y) == abs(i-x):
return False
return True
观察可见,外层循环用于寻找每行中的one-hot值,大量0值元素实为无关信息。换言之,对于二维数组board,我们真正关心的是每行中值为1的元素的下标。因此,自然想到将board转换为一维数组,利用下标直接代表行号,数组值代表该行皇后所在的列号。
若采用此数据结构,程序需进行改造,首先调整check函数以适配新的board结构:
def check(board,row,col): i = 0 while i < row: if abs(col-board[i]) in (0,abs(row-i)): return False i += 1 return True
将二维数组board改为一维数组后,我们可在O(1)时间内确定row行之前每一行皇后的位置,并以此作为参考进行冲突判断,显著降低复杂度。
主函数随之修改如下:
def EightQueen(board,row):
blen = len(board)
if row == blen: # 来到不存在的第九行了
print board
return True
col = 0
while col < blen:
if check(board,row,col):
board[row] = col
if EightQueen(board,row+1):
return True
col += 1
return False
def printBoard(board):
'''为了更友好地展示结果 方便观察'''
import sys
for i,col in enumerate(board):
sys.stdout.write('□ ' * col + '■ ' + '□ ' * (len(board) - 1 - col))
print ''
整体结构与修改前类似,区别在于主循环中,不再通过游标设置-移除单个位置的状态,而是简化为col += 1。采用col += 1的好处是:当某轮递归失败返回上层后,无需特意“收回”之前放置的皇后,只需直接执行col += 1即可继续搜索。
printBoard函数可将一维数组board的状态直观展示:
■ □ □ □ □ □ □ □
□ □ □ □ ■ □ □ □
□ □ □ □ □ □ □ ■
□ □ □ □ □ ■ □ □
□ □ ■ □ □ □ □ □
□ □ □ □ □ □ ■ □
□ ■ □ □ □ □ □ □
□ □ □ ■ □ □ □ □
所有结果
上述程序仅生成一个解,实际上八皇后问题存在多种布局。如何求得所有解?只需对原程序稍作修改即可。
以修改后的一维数组维护棋盘状态为例。原程序在遇到row == blen时返回True,递归层层返回True直至最顶层,因此找到一个解后程序即退出。
反之,若获得一个解后不判断EightQueen函数的返回值,函数将继续执行col += 1,使状态搜寻继续。收集状态的任务在row == blen的判断中完成(注意此处return不可删除,它用于提示递归终结条件)。对每条递归路径穷尽所有可能后再回溯,即可获取所有解:
def EightQueen(board,row):
blen = len(board)
if row == blen: # 来到不存在的第九行了
print board
return True
col = 0
while col < blen:
if check(board,row,col):
board[row] = col
if EightQueen(board,row+1):
# return True 去掉这里即可,或者直接删除掉整个判断,只留下单一个EightQueen(board,row+1)
pass
col += 1
return False
示例结果:
[0, 4, 7, 5, 2, 6, 1, 3] [0, 5, 7, 2, 6, 3, 1, 4] [0, 6, 3, 5, 7, 1, 4, 2] [0, 6, 4, 7, 1, 3, 5, 2] [1, 3, 5, 7, 2, 0, 6, 4] [1, 4, 6, 0, 2, 7, 5, 3] [1, 4, 6, 3, 0, 7, 5, 2] [1, 5, 0, 6, 3, 7, 2, 4] [1, 5, 7, 2, 0, 3, 6, 4] …… 总共有92种布局方案
非递归
非递归解法显然需要维护一个栈(stack)来保存路径。栈中维护的应是“尚未尝试探索的可能状态”。当检查通过特定位置时,首先将该位置右侧一格入栈,再将下一行的第一个格子入栈。注意前半步操作易被忽视:若不将本位置右侧一格入栈,若基于当前皇后的递归最终无解,后续将无法确定移动方向。使用栈后,扫描棋盘的游标无需在循环中手动+=1,其移动完全由栈维护。
代码如下:
def EightQueen(board):
blen = len(board)
stack = Queue.LifoQueue()
stack.put((0,0)) # 为了自洽的初始化
while not stack.empty():
i,j = stack.get()
if check(board,i,j): # 当检查通过
board[i] = j # 别忘了放Queen
if i >= blen - 1:
print board # i到达最后一行表明已经有了结果
break
else:
if j < blen - 1: # 虽然说要把本位置右边一格入栈,但是如果本位置已经是行末尾,那就没必要了
stack.put((i,j+1))
stack.put((i+1,0)) # 下一行第一个位置入栈,准备开始下一行的扫描
elif j < blen - 1:
stack.put((i,j+1)) # 对于未通过检验的情况,自然右移一格即可
显然,若去掉break语句,即可求得所有解。
C语言版
#include <stdio.h>
static int board[8] = {};
int board_size = sizeof(board)/sizeof(int);
int check(int *board,int row){
int i = 0;
while(i < row){
if(board[i] == board[row] || row - i == board[row] - board[i] || row - i == board[i] - board[row]){
return 0;
}
i++;
}
// printf("board[%d]: %d\n",row,board[row]);
return 1;
}
void print_board(int *board){
int i;
int size = board_size;
for(i=0;i<size;i++){
printf("%d,",board[i]);
}
printf("\n");
i = 0;
while (i < size){
int j;
for (j=0;j<size;j++){
if(j == board[i]){
printf("%s ","■ ");
}
else{
printf("%s ","□ ");
}
}
printf("\n");
i++;
}
}
int eight_queen(int *board,int row){
if (row == 8){
print_board(board);
return 1;
}
board[row] = 0;
while (1){
if (check(board,row) && eight_queen(board,row+1)){
return 1;
}
else{
if(++board[row] >= 8){
break;
}
}
}
return 0;
}
int main(){
eight_queen(board,0);
// print_board(board);
return 0;
}
总结
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对本站的支持。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













