Python实现八皇后问题:递归与非递归算法详解

远山来客
远山来客
发布时间:2026-10-13 | 5人浏览过
原创

本文深入解析八皇后问题的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;
}

总结

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对本站的支持。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表