回溯算法解决八皇后问题(C++实现)

写在开篇

本篇开始前,感谢B站up主@木子喵neko 在视频中提供的思路,有需要看视频内容的可以点击以下链接:

【neko算法课】N皇后问题 回溯法【13期】

概述

八皇后问题于1848年由国际象棋棋手马克斯·贝瑟尔于1848年提出,即在8*8的国际象棋棋盘上摆放八个皇后,要求任意两个皇后之间不能相互攻击(任意两个皇后不能处于同一行、同一列以及同一对角线上),求出一共有多少种摆放法?

下图为其中一种摆放方式:

解决思路

关于八皇后问题目前网上有很多种解决思路,但多数方法内容较繁琐且抽象,对编程初学者而言很难理解。本文中使用较为形象的方式详细说明用回溯算法如何解决八皇后问题,帮助大家以直观的方式去理解回溯算法的运算步骤。

为了更加直观地去理解,我们对8*8的国际象棋棋盘作以下标记:

在上图中棋盘中:

  • 用X表示棋盘格子所在的行号,行号从第0行至第7行,共计8行
  • 用Y表示棋盘格子所在的列号,列号从第0列至第7列,共计8列

摆放皇后的方式,按照以下规则进行摆放:

  • 在第0行选择合适位置摆放一个皇后
  • 在第1行选择合适位置摆放一个皇后
  • 在第2行选择合适位置摆放一个皇后
  • ......
  • 在第7行选择合适位置摆放一个皇后
  • 如果当前所在行找不到合适位置摆放皇后,则说明前一行的皇后摆放位置不符合要求,应当回溯到前一行皇后的摆放程序,将前一行的皇后更换摆放位置
  • 在第八个皇后摆放完成后,如果棋盘上的八个皇后满足要求(即任意两个皇后不能处于同一行、同一列以及同一对角线上),则得到一个合适的摆放方案并将其输出,然后回溯到前一行更换皇后摆放位置
  • 当程序回溯到第0行时,如果第0行皇后已经将第0列至第7列所有位置摆放完毕,则说明已经获得所有可行的摆放方案,程序结束

难点分析

不难看出,在上述摆放规则中,最难实现的要点在于:

当第X行已经选择合适的位置摆放皇后时,如何确保后续皇后所选择的摆放位置不会受到之前摆放的皇后的影响(即任意两个皇后不能处于同一行、同一列以及同一对角线上)。

因此,在每一次皇后摆放前,我们必须引入相应的判定规则,帮助计算机判定棋盘第X行第Y列所在格是否受到之前所摆放的皇后的影响。

这里,我们用X代表需要摆放皇后的格子行号,Y代表需要摆放皇后的格子列号。

我们以棋盘第0行第0列摆放第一个皇后为例进行说明:

此时,第0行所摆放的皇后所在格子的行号为0,列号为0,坐标可以表示为(0,0)。

图中红线所划过的格子为该皇后所能影响的范围,意味着红线划过的格子不能再摆放其他皇后。

通过观察可以看出,如果我们后续需要在棋盘上摆放皇后时,所摆放的格子坐标(X,Y)必须满足以下条件:

  • X≠0
  • Y≠0
  • X-Y≠0

由于我们是通过逐行选择合适的位置摆放皇后的,意味着每一行只会摆放一个皇后,因此X≠0的条件可以省略,在实际程序编写时只需考虑以下两个条件即可:

  • Y≠0(第0列为第0行所摆放皇后的直线攻击范围)
  • X-Y≠0(当所选格子坐标满足X-Y=0时,意味着该位置为第0行所摆放皇后的对角线攻击位置)

然后,我们可以在第1行第3列所在格放置一个皇后,效果如下:

不难看出,当第1行第3列所在格摆放皇后时,如果我们需要继续在棋盘上选择合适位置摆放皇后,则所摆放的格子坐标(X,Y)应当满足以下条件:

  • Y≠0 && Y≠3
  • X-Y≠0 && X-Y≠-2 && X+Y≠4

综上,我们可以得出结论:

如果第一个皇后已摆放至合适位置时(坐标为(a,b)),如果我们需要摆放第二个皇后的位置(坐标为(X,Y))只需满足以下条件,则第二个皇后摆放位置不受到第一个皇后的影响:

  • Y≠b(取值范围为0~7)
  • X-Y≠a-b(取值范围为-7~7)
  • X+Y≠a+b(取值范围为0~14)

代码分析

通过前文的分析,我们可以通过逐行扫描+位置判定+回溯的方式来判断8*8国际象棋棋盘上每一行的皇后如何进行摆放,并输出每一个可行的解决方案。

以下为C++代码,通过vs2022编译环境实现:

#include <iostream>

using namespace std;

const int chessBoard = 8;
static int totalSolution = 0;
static int queenColumn[chessBoard];
static bool ColumnOccupied[chessBoard];
static bool s1[chessBoard * 2];
static bool s2[chessBoard * 2];

void Init();
void putQueen(int x);
void printSolution();

void Init() {
	for (int i = 0; i < chessBoard; i++) {
		queenColumn[i] = -1;
		ColumnOccupied[i] = false;
	}
	for (int j = 0; j < chessBoard * 2; j++) {
		s1[j] = s2[j] = false;
	}
}

void putQueen(int x) {
	for (int y = 0; y < chessBoard; y++) {
		if (!ColumnOccupied[y] && !s1[chessBoard + x - y] && !s2[x + y]) {
			queenColumn[x] = y;
			ColumnOccupied[y] = s1[chessBoard + x - y] = s2[x + y] = true;
			if (x < chessBoard - 1) putQueen(x + 1);
			else printSolution(); 
			queenColumn[x] = -1;
			ColumnOccupied[y] = s1[chessBoard + x - y] = s2[x + y] = false;
		}
	}
}

void printSolution() {
	cout << "第" << ++totalSolution << "种解决方案:" << endl;
	for (int i = 0; i < chessBoard; i++)
	{
		for (int j = 0; j < chessBoard; j++)
		{
			if (queenColumn[i] == j) cout << "Q ";
			else cout << "- ";
		}
		cout << endl;
	}
	cout << endl;
}

int main() {
	Init();
	putQueen(0);
	system("pause");
	return 0;
}

在这一章节,本文会逐步对上述代码进行分析,会使用较为直观的方式帮助读者理解该代码是如何实现皇后的排放及步骤回溯。

变量声明

//棋盘每行每列格子数为8
const int chessBoard = 8;
//统计解决方案数
static int totalSolution = 0;
//用于存放所摆放的皇后的所在列数(取值范围0~7)
static int queenColumn[chessBoard];
//用于判定所需要放置皇后是否与先前摆放的皇后处于同一列
static bool ColumnOccupied[chessBoard];
//用于判定所需要放置皇后是否与先前摆放的皇后处于同一对角线
static bool s1[chessBoard * 2];
static bool s2[chessBoard * 2];

在本代码中,我们事先声明了queen的整形数组用于存放每一行皇后所在的列数(取值范围为0~7)。同时又声明了columnOccupied、s1、s2的布尔型数组用于条件判定。columnOccupied[y]的值为true表示第y行已经摆放了皇后,s1[chessBoard+x-y]、s2[x+y]的值为true表示第x行第y列所在格的对角线上已经摆放了皇后。

这里说明下,根据先前所述的皇后攻击位置判断条件,由于x-y的范围存在负数,为了确保判定条件数组s1的正常使用,代码中补上了一个整数(变量chessBoard)以确保s1的索引取值范围内没有负数,该步骤并不影响判定条件s1数组的正常使用。

初始化

//初始化
void Init() {
	for (int i = 0; i < chessBoard; i++) {
        //每行皇后所在列位置清空,皇后所在列的判定条件初始化
		queenColumn[i] = -1;
		ColumnOccupied[i] = false;
	}
	for (int j = 0; j < chessBoard * 2; j++) {
        //皇后对角线判定条件初始化
		s1[j] = s2[j] = false;
	}
}

皇后的摆放及回溯

/**
 *逐行扫描,选择合适位置放置皇后,如果任一行无合适位置摆放皇后,则回溯至上一行重新选择位置摆放。
 *参数x:棋盘行数(取值范围0~7,共计8行)
*/
void putQueen(int x) {    //从第x行开始扫描合适位置放置皇后。
	for (int y = 0; y < chessBoard; y++) {
        /* 如果第x行第y列位置没有受到先前所摆放皇后攻击范围的影响,
           则列判定条件ColumnOccupied[y]、s1[chessBoard + x - y]、
           以及s2[x + y]的取值均为false。 */
		if (!ColumnOccupied[y] && !s1[chessBoard + x - y] && !s2[x + y]) {
            //找到合适位置后放置皇后,记录第x行所放置皇后的所在列数queenColumn[x]。
			queenColumn[x] = y;
            //当前第x行第y列放置皇后的攻击范围均赋值为true。
			ColumnOccupied[y] = s1[chessBoard + x - y] = s2[x + y] = true;

            //------------------分隔线----------------

            //任一行皇后放置完毕后,应先判断该行是不是最后一行。
            //如果不是最后一行,则递归摆放第x+1行的皇后。
			if (x < chessBoard - 1) putQueen(x + 1);
            //如果第x行是最后一行,说明已经成功完成一个解决方案,调用printSolution()方法打印该解决方案。
			else printSolution(); 

            //执行回溯
            //将第x行第y列所摆放的皇后清除,同时将第x行第y列皇后的攻击判断条件清空(赋值为false)
			queenColumn[x] = -1;
			ColumnOccupied[y] = s1[chessBoard + x - y] = s2[x + y] = false;
		}
	}
}

初学者可能会好奇,上面这两行代码是如何实现回溯功能的。接下来,这里较为直观地讲解该代码是如何实现回溯功能。

以8*8国际象棋棋盘为例,通过上述putQueen()方法的代码可知,当执行putQueen(0)时表示程序从第一行开始遍历各列数寻找合适位置放置皇后,当找到合适位置时,先放置皇后,后递归执行putQueen(1)方法从第二行开始遍历各列寻找合适位置放置皇后;当第二行皇后放置完成时,后递归执行putQueen(3)方法从第三行开始遍历皇后位置……以此类推,当递归执行putQueen(x)行时,此时计算机栈内存中putQueen()方法的排列方式如下:

此时,会出现以下几种情况:

一、当x<7时,如果putQueen(x)在第x+1行经遍历后找不到合适位置放置皇后,说明在第x行的皇后位置放置有误需更换下一个合适位置,此时在栈内存中putQueen(x)方法弹出栈内存,程序会返回至putQueen(x-1)方法当中去继续执行上述两条回溯语句,用于清空第x行已摆放的皇后,同时将第x行摆放皇后的攻击判断条件也清空,然后继续执行putQueen(x-1)方法中的for语句继续遍历可能的合适位置;如果经遍历后在第x行找到新位置摆放完皇后,则继续递归执行putQueen(x)方法;如果经遍历后在第x行找不到合适位置,说明在第x-1行的皇后位置放置有误需更换下一个合适位置,此时在栈内存中putQueen(x-1)方法弹出栈内存,程序会返回至putQueen(x-2)方法当中去继续执行回溯语句……以此类推,直至返回至putQueen(0)方法在第一行继续遍历合适位置放置皇后。

二、当x=7且putQueen(7)方法已经在第八行找到合适位置摆放皇后,说明找到一个合适的解决方案,然后程序执行printSolution()方法在控制台打印解决方案;打印完成后,此时程序会执行上述回溯语句,将第八行已摆放的皇后位置以及攻击判断条件清空,再继续通过for语句继续遍历第八行可能的合适位置;如果在第八行找到新位置则继续摆放皇后并打印解决方案,如果找不到新位置则返回至putQueen(6)方法当中执行回溯语句并遍历第七行新位置摆放皇后……以此类推,直至返回至putQueen(0)方法在第一行继续遍历合适位置放置皇后。

三、当x=0且putQueen(0)方法在第一行已经找不到合适位置摆放皇后,说明此时程序已找到全部的解决方案,putQueen(0)方法弹出栈内存,结束所有putQueen()方法。

打印解决方案

//打印解决方案
void printSolution() {
    //该方法每执行一次,解决方案记录数自动加一。
	cout << "第" << ++totalSolution << "种解决方案:" << endl;
    //第i行第j列进行遍历
	for (int i = 0; i < chessBoard; i++)
	{
		for (int j = 0; j < chessBoard; j++)
		{
            //遍历到该位置放置皇后的,输出'Q'表示皇后,输出'-'表示空位置。
			if (queenColumn[i] == j) cout << "Q ";
			else cout << "- ";
		}
		cout << endl;
	}
	cout << endl;
}

main方法

int main() {
	Init();        //初始化
	putQueen(0);   //从第一行开始扫描并放置皇后
	system("pause");
	return 0;
}

输出结果

该代码执行后效果如下:

综上,在8*8的国际象棋棋盘上放置8个互不攻击的皇后,可行的方案有92中。

该代码可以用于计算在N*N的国际象棋棋盘上放置N个互不攻击的皇后,可以有多少种解决方案。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值