今天想写一篇有关回溯法的问题,但是不知道开头应该介绍些什么东西。所以简单粗暴一点,直接上问题。一种经典的回溯算法就是八皇后问题,何为八皇后问题呢?
八皇后问题是一个以国际象棋为背景的问题:如何能够在8×8的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上。 ——引自维基百科
为了更好的理解,上一张图:
图中画×的是满足条件的,画〇则不满足,因为有两个在同一条直线上。那么问题来了,在一个8 * 8的棋盘上符合条件的八皇后到底有多少种摆法呢?由此便衍生出了计算机中的八皇后问题。
首先,我们的基本思路就是先摆第一个,再摆第二个,依此类推,如果第二个不符合条件,则调整第一个的位置(回溯)。简而言之,我们将8 * 8的棋盘看成是一个二维数组,首先将第一个棋子摆在[0][0]的位置。再将第二个摆在[1][0]的位置(因为第二个位置一定不能再摆在0开头的位置,否则一定在同一行),如果不合适再放在[1][1]的位置,依次类推,如果摆到[1][7]的位置还有冲突,则需要调整第一个棋子的位置至[0][1],再依次尝试,这就是递归的基本原理,将其转换成代码如下:
for(int i = 0; i < QueenNum; i++){ //通过循环,放置每一个棋子
column[currentQueen] = i; //试探性的将当前的棋子放在第i列
if(hasConflict()){ //判断和之前的是否有冲突,若有冲突直接continue,重新放置
continue;
}
else //没有冲突则直接进入迭代放置下一个棋子
putQueen();
}
另外一个需要解决的问题就是冲突检测,如何检查是否有冲突呢?我们的基本思路就是,每排只放一个,这样保证了不会有任意2个棋子在同一行(即第i个棋子摆在弟i行)。其次,每摆一个棋子后,我们用一个数组将其列数记下来,如column[i]表示第i个棋子摆放的列数,因此当我们摆放第i+1个棋子时就需要判断这个棋子是否和column这个数组中的0~i个元素有冲突,若没有则将column[i + 1]置为当前棋子摆放的列数。基于这个思路我们可以写出以下代码:
bool hasConflict = false;
for(int i = 0; i < currentQueenNum; i++){ //currentQueenNum表示当前棋子的个数,遍历数组中已经存放的每个棋子所在的列数,i表示棋子的行数
if(column[i] == currentQueenColumn || abs(column[i] - currentQueenClumn) == abs(i - currentQueenNum){ //前面一部分判断是否在同一列,后面一部分判断是否在同一斜线上
hasConflict = true; //如果有,则将有冲突标志置为true,并终止判断
break;
}
}
解决了这两块问题,接下来就是将他们整合以下,既然上面有迭代代码,那么就必须有一个终止迭代的条件,否则整个迭代会一直循环下去,永无停止。显而易见,迭代终止的条件就是,当currentQueenNum == QueenNum时终止迭代,即所有的皇后都放置完了。
完整代码如下:
#include <iostream>
#include<math.h>
#define MAXQUEEN 100
int checkerBord[MAXQUEEN]; //定义一个棋盘最大可容纳多少个皇后
int methods = 0; //全局变量,记录一共有多少种方案
void putQueen(int queenNum, int currentQueenNum) { //放置皇后,需要给出两个参数,一个是需要放置多少个皇后,一个是当前放置的是哪一个皇后
if (queenNum == currentQueenNum) //迭代终止条件,一旦迭代终止了,则表示放置方式符合条件方案加1
methods++;
else {
for (int i = 0; i < queenNum; i++) {
checkerBord[currentQueenNum] = i; //逐行模拟放置
bool hasConflict = false;
for (int j = 0; j < currentQueenNum; j++) { //检测冲突
if (checkerBord[j] == i || abs(currentQueenNum - j) == abs(checkerBord[j] - i)) {
hasConflict = true;
break;
}
}
if (hasConflict) //如果有冲突,则继续重新放置,否则放置下一个
continue;
else
putQueen(queenNum, currentQueenNum + 1);
}
}
}
int main()
{
int queenNum;
std::cout << "请输入需要放置的皇后的个数(0~100):\t";
std::cin >> queenNum;
putQueen(queenNum, 0); //main程序中,假设共有8个皇后,需要放置的皇后为0~7
std::cout << "当皇后个数为" << queenNum << "时,共有" << methods << "种方案";
}
输出如下:
那么,我们能不能看到这么多的方案分别是怎么摆的呢?既然我敢提出来那答案当然是可以啦。从上面的介绍种我们能够知道,满足迭代终止条件时,肯定是符合条件的。那么在这种情况下我们只要按照存储在checkerboard中的列的摆放位置就可以了,我们约定空位置用〇表示,放置了皇后的位置用×表示,那么将上面的迭代终止的内容改一下就OK了,代码如下:
if (queenNum == currentQueenNum) {
methods++;
std::cout << "方案" << methods << ": \n";
for (int i = 0; i < queenNum; i++) { //行数
for (int j = 0; j < queenNum; j++) { //列数
if (checkerBord[i] == j) //有放置棋子
std::cout << "× ";
else //无棋子
std::cout << "〇 ";
}
std::cout << std::endl;
}
std::cout << "\n\n";
}
以上便是著名的八皇后问题的解法。