回溯法求n皇后问题(递归、非递归及优化)
n皇后问题是一个以国际象棋为背景的问题:在n×n的国际象棋棋盘上放置n个皇后,使得任何一个皇后都无法直接吃掉其他的皇后,即任意两个皇后都不能处于同一条横行、纵行或斜线上。
蛮力法思想:
解决n皇后问题的思想本质上就是蛮力法,生成所有可能的摆放情况,并判断该情况是否满足要求,我们以树结构来表示解决问题的方法。以4*4的棋盘为例,第0层的根节点为空白的棋盘,第1层为只在棋盘的第一行摆放的四种不同情况,第2层是在第1层的基础上,摆放第二行的棋子,最后的叶子结点便是所有可能的完整摆放情况,共256种,但加上中途生成的不完整情况共1+4+16+64+256=340种。
回溯法思想:
回溯法其实是以蛮力法为基础,只是不需要生成所有的情况,我们可以发现,在整棵树中,有些棋盘的摆放情况在未达到叶子结点时,便已经不满足n皇后的条件了,那么我们就没有必要再去往下摆放棋子(生成下一层的结点),而是直接回到该结点的父节点,生成新的情况进行判断。通过这种方法可以减少生成完全树中的一些不必要的子树,我们称之为“剪枝”。具体实现中回溯法与蛮力法的主要区别在于判断棋盘的代码所在的位置,蛮力法在摆放完所有皇后后再判断,回溯法在每摆放好一个皇后时就进行判断。
具体实现:
根据n皇后问题的规模,创建大小为n的数组代替树结构,使其下标代表行数,内容代表列数,数组中的每个数必定位于不同的行,数组内容不同证明两个元素位于不同的列,两数下标的差的绝对值不等于两数内容的差的绝对值,表示两数不位于同一斜线上。
int *QUEEN = new int[scale + 1];//scale为n皇后问题中的n
回溯法的判断方法:
bool judgeQueenBT(int line) //回溯法判断函数,一重循环判断当前皇后与前面任一皇后是否满足条件,line为当前所在行
{
for (int i = 1; i < line; i++) {
if (QUEEN[i] == QUEEN[line] //判断是否在同一列上
|| abs(QUEEN[i] - QUEEN[line]) == line - i) //判断是否在对角线上
return false;
}
return true;
}
递归实现:
void setQueenBT(int line) //回溯法摆放皇后,line为当所在行
{
if (line > scale) { //表示已摆放完所有皇后,直接输出答案,scale为n皇后的规模n
displayQueenBT(line - 1);
return;
}
else {
for (int i = 1; i <= scale; i++) {
nodeNum++;
QUEEN[line] = i;
if (judgeQueenBT(line)) //每次摆放一个皇后之后就要进行判断
setQueenBT(line + 1);
}
}
}
非递归实现:
void setQueenBT_2(int line) //非递归回溯法摆放皇后
{
int row = 1; //row表示当前行数,通过row的变化更改数组中的值,当row大于规模时表示得出一个答案,当row小于1时表示全部遍历完成
QUEEN[1] = 1;
while (row > 0) {
if (row <= line && QUEEN[row] <= line) { //当row小于规模和数组内容小于规模,说明当前row的指示还在棋盘中
if (!judgeQueenBT(row))
QUEEN[row]++;
else {
row++; //进入下一行
QUEEN[row] = 1; //列数置为1
}
nodeNum++;
}
else {
if (row > line) {
displayQueenBT(line);
}
row--; //回到上一行
QUEEN[row]++; //并且列数增加
}
}
}
在不输出摆放情况的时候,非递归方法的时间效率要优于递归方法,因为算法本身的时间会远小于io输出的时间,所以在需要输出摆放情况的情况下,运行时会看不出算法时间上的缩短。
镜像优化:因为每一个符合条件的摆放情况,都有一个与之对称的情况,所以可以只求一半的答案,即只求解第一行前一半有摆放的情况。
void setQueenBT_3() //镜像优化回溯法摆放皇后第一行
{
int l = scale / 2; //第一行只取前半部分的位置进行摆放皇后
for (int i = 1; i <= l; i++) {
nodeNum++;
QUEEN[1] = i;
setQueenBT_3_1(2); //对称部分调用镜像回溯法函数,该函数与其他函数的区别在于输出的结果包含镜像答案的结果
}
if(scale%2==1){ //若棋盘规模为奇数还得计算中间部分的答案数
l++; //l = scale/2 + 1
nodeNum++;
QUEEN[1] = l;
if (judgeQueenBT(1))
setQueenBT(2); //中间部分调用普通的回溯法函数
}
}
void setQueenBT_3_1(int line) //镜像优化回溯法摆放皇后
{
if (line > scale) {
displayQueenBT_3(line - 1); //该方法用于输出答案本身和该答案的对称答案
return;
}
else {
for (int i = 1; i <= scale; i++) {
nodeNum++;
QUEEN[line] = i;
if (judgeQueenBT(line))
setQueenBT_3_1(line + 1);
}
}
}
需要注意的是,该方法理论上会减少运算量,但只存在于只需求解答案个数和生成答案情况的时候,如果要求输出另一半的摆放情况,因为算法本身的时间会远小于io输出的时间,所以在真正运行时会看不出算法时间上的缩短。
附近优化:
直接判断所消耗的时间会小于生成结点再判断,而且当摆放好一个棋子时,该棋子下方的三个位置都不能再放棋子,所以可以跳过这三个位置。(当然也有其他更好的方法,比如跳过棋子所在的一整列)
void setQueenBT_4() //附近优化回溯法摆放皇后第一行
{
for (int i = 1; i <= scale; i++) {
nodeNum++;
QUEEN[1] = i;
setQueenBT_4_1(2);
}
}
void setQueenBT_4_1(int line) //附近优化回溯法摆放皇后
{
if (line > scale) {
displayQueenBT(line - 1);
return;
}
else {
int i;
for (i = 1; i <=scale; i++) {
if (i >= QUEEN[line - 1] - 1 && i <= QUEEN[line - 1] + 1) //若判断到该点是上一个皇后下方的三点,则跳过
continue;
nodeNum++;
QUEEN[line] = i;
if (judgeQueenBT(line))
setQueenBT_4_1(line + 1);
}
}
}
需要注意的是因为每次只跳过三个位置(在边缘时只有两个),所以随着n皇后的规模越大,该方法的优化效果越不明显。下一篇: 自动调参神器NNI
推荐阅读