n皇后问题 递归和非递归

问题引入 八皇后问题

八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。

问题中涉及了回溯算法,它就是一种深度优先搜索算法,每走一步,选择其中的一种可能解,满足就继续走下一步,不满足就回到上一步选择另一种可能的解

下图有助于理解皇后问题的回溯法解决(有其它的解法)

n皇后问题 递归和非递归
在该问题中了解了回溯算法的流程之后我们需要考虑什么时候回溯,也就是每次放置皇后需要考虑的问题:

放置皇后时是否存在同一行,同一列,互为为对角线位置的皇后存在?

假设该皇后放置在坐标(i,j)的位置,每次检查时检查前i-1行就不会涉及到同一行的情况,同一列问题只需要在放置前n-i行的时候标记皇后所在的列数就能解决,而对角线位置则需要前i-1个皇后的坐标与当前皇后的坐标斜率不为正负1

在很多情况下八皇后都被推广到n皇后、2n皇后等,并且有一些附加的条件需要考虑,但是归根到底都是对回溯算法的一种考查。

先看看非递归版的

这里我以我洛谷的一道题目 P1219八皇后 来展示N皇后的非递归解法
n皇后问题 递归和非递归

输入输出样例

输入
6
输出
2 4 6 1 3 5
3 6 2 5 1 4
4 1 5 2 6 3
4

下方是我的题解和注释

#include<iostream>
#include<cmath>
#include<cstring>
using namespace std;
int queen[14];	//判断行
int col[14];//判断列,一开始以queen[i]的值表示皇后位于第几列(递归版的就是这么做的)
	//但是每次检查的时候都要遍历queen[],最后一个测试点超时了,改了一下,增加了一个数组进行表示
int n;
int total = 0;
bool HasSpace(int i, int j)		//检查当前棋盘格子是否可以放置皇后
{
	if (col[j])return false;	//检查同一列
	for (int k = 1; k < i; k++)	//检查时只要检查到第i行之前
			if (abs(k - i) == abs(queen[k] - j))return false;		//对角线为正负1
	return true;
}

void Print()		//打印皇后的位置
{
	for (int i = 1; i <= n; i++)
		cout << queen[i] << " ";
	cout << endl;
}

int main()
{
	int i, j;
	int temp = 0;	//temp初始化为0
	//memset(queen,0,sizeof(queen));		因为queen[]和col[]是全局变量自动初始化为0
	//memset(col, 0, sizeof(col));		如果不是设置为全局的需要手动初始化
	cin >> n;
	for (i = 1; i <= n; i++)
	{
		int flag = 1;
		for (j = temp + 1; j <= n; j++)	
		{
			temp = 0;	//用完temp重置为0
			if (HasSpace(i, j)) {
				queen[i] = j;
				col[j] = 1;	//表示该列有皇后
				flag = 0;	//表示可以放
				if (i == n) {	//满足n个皇后都放完
					total++;
					if (total <= 3)Print();		//根据题意打印前三种情况
					/*完成一种情况,回溯*/
					//每次回溯都要把上一行的标记去除,成功的情况下当前行的标记也要去掉
					col[queen[i]] = 0;	//去掉当前行标记
					queen[i] = 0;	
					i -= 2;		//去掉上一行标记
					temp = queen[i + 1];	//记录回溯到上一个的位置
					col[temp] = 0;	
					queen[i + 1] = 0;
				}
				break;
			}
		}
		if (flag) {		//没找到,回溯
			if (i == 1 && j == n + 1) {
				break;	//回溯到第一行末表示结束
			}
			i -= 2;		//去掉上一行标记,因为第一个for循环后面有i++的条件,所以这里i减少2
			temp = queen[i + 1];	//记录回溯到上一个的位置,下一次循环j从temp+1开始
			col[temp] = 0;	//将该列置0
			queen[i + 1] = 0;
		}
	}
	cout << total << endl;
	return 0;
}

递归版

递归版的算法一般比较代码整洁,效率差,理解起来也不太容易
下方代码采用C语言描述(因为大学课程安排问题,平常都是几种语言混用)
(注意:此代码并非针对上方的P1219八皇后题目的题解)

#include<stdio.h>
#include<math.h>
#define MAX 6
int queen[MAX];	//下标代表行,值代表列(注意下标从0开始)

int HasSpace(int n)	//n,queen[n],n皇后是否能放置在queen[n]列上
{
	int i;
	for ( i = 0; i < n ; i++)	//扫描前n-1行
	{
		/*queen[i]==queen[n]表示同一列有皇后  两个abs相等表示有位于同一对角线的皇后*/
		if (queen[i] == queen[n] || abs(i - n) == abs(queen[i] - queen[n]))
		{
			return 0;
		}
	}
	return 1;
}

void PrintfQueen()
{
	int i;
	for (i = 0; i < MAX; i++)		//输出每一行皇后的位置
		printf("%d", queen[i]+1);	//数组的下标从0开始,输出结果要加1
	printf("\n");
}

void PlaceQueen(int n)	//表示放置第n个皇后
{
	/*
	看起来并没有非递归版的直观易理解,
	因为它的回溯过程是通过递归时的压栈出栈来实现的
	当一个皇后放置成功,进行PlaceQueen(n+1)放置下一个皇后
	当该皇后放置失败,会跳出当前的PlaceQueen(n+1)回到PlaceQueen(n)然后j++继续搜索
	可以模拟一下递归过程来理解
	*/
	int j;
	for (j = 0; j < MAX; j++)
	{
		queen[n] = j;	//先把皇后放下去,再判断是否放置正确
		if (HasSpace(n)) {	//如果放置正确,分两种情况
			if (n == MAX - 1)PrintfQueen();	//放满则打印
			else PlaceQueen(n + 1);	//否则放置下一个皇后
		}
	}
}
int main()
{
	PlaceQueen(0);	//从第一行的皇后开始放
	return 0;
}

最后

前文也有提过n皇后问题有其它的解法
例如通过位运算和数学构造法,奈何笔者才疏学浅,感兴趣的可以自己搜索

上一篇:[题解]N 皇后问题总结


下一篇:Python 解决八皇后问题