问题
八皇后问题
八皇后问题(英文:Eight queens),是由国际西洋棋棋手马克斯·贝瑟尔于1848年提出的问题,是回溯算法的典型案例。
问题表述为:在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。如果经过±90度、±180度旋转,和对角线对称变换的摆法看成一类,共有42类。计算机发明后,有多种计算机语言可以编程解决此问题。
——引自百度百科
有大佬在我的博文下面评论说八皇后特别离谱,所以就略微产生了点兴趣。结论是:八皇后问题确实离谱……写了起码有两个半小时吧
参考的是百度百科里面介绍的回溯算法。百度百科里的C++程序有够离谱,就不贴出来了,而其它语言写的简洁得离谱……不知道用什么算法实现的,我也没细看。
思路
将列A的皇后放在第一行以后,列B的皇后放在第一行已经发生冲突。这时候不必继续放列C的皇后,而是调整列B的皇后到第二行,继续冲突放第三行,不冲突了才开始进入列C。如此可依次放下列A至E的皇后,如图2所示。将每个皇后往右边横向、斜向攻击的点位用叉标记,发现列F的皇后无处安身。这时回溯到列E的皇后,将其位置由第4行调整为第8行,进入列F,发现皇后依然无处安身,再次回溯列E。此时列E已经枚举完所有情况,回溯至列D,将其由第2行移至第7行,再进入列E继续。按此算法流程最终找到如图3所示的解,成功在棋盘里放下了8个“和平共处”的皇后。继续找完全部的解共92个。
回溯算法求解八皇后问题的原则是:有冲突解决冲突,没有冲突往前走,无路可走往回退,走到最后是答案。为了加快有无冲突的判断速度,可以给每行和两个方向的每条对角线是否有皇后占据建立标志数组。放下一个新皇后做标志,回溯时挪动一个旧皇后清除标志。
以上是百度百科中对回溯算法的解释,也是我的算法的根据。
由于皇后能攻击同一行/列的所有位置,因此只需考虑每一列只放一个皇后的情况。
代码
以下是源代码
#include <iostream>
using namespace std;
void setChess(int board[8][8], int letter, int num){ //放棋子,同时设置棋子的攻击范围
if (board[letter][num] != 0){
cout << "Here is a chess or here is attacked!\n";
return;
}
board[letter][num] = 1;
for (int i = 1; letter + i < 8; i++){
board[letter + i][num]--;
if (num + i < 8)
board[letter + i][num + i]--;
if (num - i >= 0)
board[letter + i][num - i]--;
}
}
void moveChess(int board[8][8], int letter, int num){ //移除棋子,同时调整被攻击范围
if (board[letter][num] != 1){
cout << "Here is no chess!\n";
return;
}
board[letter][num] = 0;
for (int i = 1; letter + i < 8; i++){
board[letter + i][num]++;
if (num + i < 8)
board[letter + i][num + i]++;
if (num - i >= 0)
board[letter + i][num - i]++;
}
}
int findPoint(int board[8][8], int letter, int num){ //寻找能放棋子的位置(数字坐标)
for (int n = num + 1; n < 8; n++){
if (board[letter][n] == 0){
return n;
break;
}
}
return -1;
}
bool reback(int board[8][8], int *queen, int &l, int &n){ //回溯悔棋
l--; //回到上一列
n = queen[l]; //读取当前列的皇后位置
moveChess(board, l, n); //移除此皇后
n = findPoint(board, l, n); //从这一列原皇后位置寻找下一个可放位置
if (n >= 0 && n < 8){
return true; //判断这一列是否存在下一个可放位置
}
else { //否则
if (l > 0){ //若没有回溯到第1列
queen[l] = -1; //设定此列没有皇后
return reback(board, queen, l, n); //再次回溯
}
else return false; //若回溯到第一列,终止回溯,并向主函数传递信息让循环结束
} //启用回溯的条件是在该列没有找到其它能放棋子的地方(在主函数中规定)
}
bool nextChess(int board[8][8], int *queen, int &l, int &n){
setChess(board, l, n); //在指定位置放棋
queen[l] = n; //标记棋子的位置
l++; //到下一列
n = findPoint(board, l, -1); //寻找能放棋子的位置
if (n >= 0 && n < 8 && l < 8){
return true; //如果有,返回真,表示找到了
} else {
return false; //如果没有,返回假,要求主函数回溯
}
}
void showBoard(int board[8][8]) { //打印棋盘
for (int i = 0; i < 8; i++){
for (int j = 0; j < 8; j++){
if (board[i][j] == 1) cout << " # ";
else cout << " - ";
}
cout << endl;
}
cout << endl;
}
int main(){
int queen[8] = { -1 };
bool flag;
int count = 0, l = 0, n = 0;
int board[8][8] = { 0 };
while (1){
flag = nextChess(board, queen, l, n); //判断下一列是否有放棋子的位置(同时放置棋子)
if (flag && l < 7){ //如果有,且还没到最后一列
continue;
} else if (flag && l == 7){ //如果放棋子放到了最后一列
count++; //表示找到了一个解
setChess(board, l, n); //放棋子
showBoard(board); //打印棋谱
moveChess(board, l, n); //移除最后一列的棋子
reback(board, queen, l, n); //从最后一列开始回溯,寻找下一个解
} else if (!flag) //如果没有
flag = reback(board, queen, l, n); //回溯,并判断是否还能回溯
if (!flag) //如果不能再回溯(即已经没有其它解时)
break; //跳出循环
}
cout << count << endl;
}
解释
以下是对各函数和变量的解释
代码中用8*8的二维数组board表示棋盘和棋盘上各点的状态,0表示该位置既无棋子也没有能攻击到此处的棋子;负数表示能攻击到该位置的棋子的个数,即-1表示有一个棋子能攻击到,-3表示有三个棋子能攻击到;1表示该位置有一个棋子。而board的第一个下标用l(letter)表示,第二个下标用n(num)表示,因为国际象棋的棋盘横坐标是字母,纵坐标是数字。此处用[0][0]~[7][7]表示国际象棋的[a][1]~[h][8]坐标。
用queen数组表示各列放置棋子的位置,如queen[2] == 3表示C4格放置了一个棋子。
第一、第二个函数(setChess,moveChess)会在将指定位置修改棋盘的棋子状态同时,将其右边的攻击范围使用将坐标的数值-1或+1的方式标记出来,这么做的好处是,移除棋子时方便确定某个位置是否还有其它棋子攻击。例如:若有2个棋子可以攻击D5格,则borad[3][4] == -2,移除掉其中一个棋子,则只剩1个棋子能攻击到D5格,在moveChess函数中,board[3][4]++,变为-1,当两个棋子都被移除掉时,board[3][4] == 0,该位置处于不被攻击的状态。由于皇后与皇后之间能否攻击是相互的,即若一个皇后能攻击另一个皇后,则另一个皇后也能攻击该皇后,所以在设置攻击范围时,而且当放置一个棋子时,其左边的列必然已都有棋子,故只需调整该皇后右边棋盘的格子状态。
findPoint函数用于寻找从指定列的指定位置的下一位置开始寻找能放置棋子的位置,并返回其位置n下标值。棋盘中只有状态为0的位置能放置棋子。若未找到能放置棋子的位置,则返回-1,用于表示无可放置位置。
reback函数用于回溯(悔棋),(reback是我瞎编的词),具体过程见注释。看得出来,reback是个递归函数,这也是为什么要用函数写的最大原因。回溯的目的有二:一是当下一列没有可以放棋子的位置时,通过悔棋来寻找能满足条件的下法;二是当找到一个解后,通过悔棋来寻找下一个解。回溯的特点是,回溯后不可能出现与先前出现过的棋盘状态相同的棋盘,因为它必然修改某一列的棋子位置(事实上在此程序中,修改位置不在reback函数中执行,reback函数只会修改l值和n值,直到borad[l][n]满足放棋子的条件,放棋子的步骤一直在nextChess中执行,这是为了出现避免重复放棋子的现象),这也是为何可以用回溯寻找下一个解的原因。
nextChess函数用于放置棋子并寻找下一个可放棋子的位置。具体过程见注释。
showBoard函数用于打印棋盘,在求出一个解时使用。
count用于统计解的数量。
此代码足足有98行,不能算是简单的算法。百度百科里用其它语言写的代码,有的十分简短,有兴趣的可以去看看,反正我是没啥兴趣看了。
如有疑问或者疏漏错误,还请各位大佬评论指出
Created by goolwind on 2021/1/12
5万+

被折叠的 条评论
为什么被折叠?



