八皇后问题——回溯法
八皇后问题
众所周知国际象棋是一种经典而著名的二人对弈的棋类游戏,相信这个不必我多介绍。棋子共有国王、皇后、战车、主教、骑士、禁卫军这七种,不仅出现于国际象棋的棋盘上,在其他领域的作品中也会用这些棋子做点文章,例如《逆转检事2》的逻辑象棋系统(御剑检察官的脑洞)。
不过我仍然不是来向你推荐游戏的,而是想要介绍从国际象棋中引申的一个问题。皇后国际象棋棋局中实力最强的一种棋子,可横直斜走,且格数不限,吃子与走法相同。也就是说,想要不被皇后棋子吃,就必须不能和该棋子在同一行、列、斜方向上。现在让我们来看看八皇后问题。
八皇后问题,一个古老而著名的问题,是回溯算法的典型案例。该问题由国际西洋棋棋手马克斯·贝瑟尔于 1848 年提出:在 8×8 格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。高斯认为有 76 种方案。1854 年在柏林的象棋杂志上不同的作者发表了 40 种不同的解,后来有人用图论的方法解出 92 种结果。
-- 百度百科
八皇后问题如果用穷举法解决,就需要验证 88 =16777216 种情况。每一列放一个皇后,可以放在第 1 ~ 8 行,穷举的时候从所有皇后都放在第 1 行的方案开始,检验皇后之间是否属于同一行、同一列、同一斜向,如果属于,就把其中一个皇后皇后挪一格,验证下一个方案……这种方法的时间复杂度无疑是很庞大的。该方法时间复杂度 O(nn)。
模拟实现
八皇后问题可以推广至 n 皇后问题,接下来我们先用 4 × 4 的棋盘来模拟一遍棋子的安放。我们先把目光聚焦在第一行。首先在第一行第一列,也就是 (1,1) 位置放置棋子。
为了更加直观,我把不允许放置棋子的位置涂上红色,需要注意的是,计算机可不能向我们这样用肉眼就能看出哪里不能放,需要进行遍历。
接下来我就需要到第二行找放置棋子的位置,我先放置于 (2,3)。
接下来我就需要到第三行找放置棋子的位置,我先放置于 (3,2)。
接下来我就需要到第四行找放置棋子的位置,不过这个时候已经没有位置可放了,也就是说我们找到了一个不可行的方式,这个时候就需要退回上一步。
接下来我就需要到第三行的 (3,2) 之后找放置棋子的位置,同样没位置可放了,退回上一步。
接下来我就需要到第三行的 (2,3) 之后找放置棋子的位置,放置于 (2,4)。
接下来操作同上,我们发现换了路线之后仍然不可行。
这个时候就说明了,在第一行的 (1,1) 位置放置第一个皇后的话,将不会有符合要求的放法,所以回溯到一开始,在 (1,2) 位置放置棋子。
重复上述操作。
这时候我们发现了,我们找到了一个可行的放置方式。
不过问题还没有结束,我们继续进行模拟。由于在第一行 (1,2) 位置放置棋子只有这一种情况,因此进行回溯,在 (1,3) 位置放置棋子。
我想你可能已经发现了,这种情况和在 (1,2) 放置棋子的情况是镜像对称的,我们还能够得到一种解法。
由于在第一行 (1,3) 位置放置棋子只有这一种情况,因此进行回溯,在 (1,4) 位置放置棋子。
这种情况和在 (1,1) 放置棋子的情况是镜像对称的,没有增加解法。由于第一行已经没有更多的位置了,模拟结束,得到两组解。
思路解析
我们观察下皇后问题的一个解法,我们发现由于需要让皇后棋子之间不能互吃,因此在每一行、每一列中只能出现一个皇后。再来分析斜向的规律,当两个棋子的坐标关系满足行数之差的绝对值等于列数之差的绝对值时,说明两个棋子在斜向上是属于同一方向。在组织数据的时候,由于我们需要知道每一种解法的具体内容,因此不能用 STL 库的 stack 容器来实现,因为这样数据不会被保存,我的解法是用 STL 库的 vector 容器来模拟栈结构,另外定义一个 top 游标来指向栈顶位置。由于每一行有且仅有一个皇后棋子,因此我们可以用容器的索引描述棋盘行数,用索引对应的值来描述列数,由此来判断是否同一列、同一斜向就会变得方便许多。
在这里我们可以使用回溯法来解决问题,回溯算法解决问题的思想是有冲突解决冲突,没有冲突往前走,无路可走往回退,走到最后是答案。我们回忆一下栈结构实现的迷宫寻路算法,我们发现这是有异曲同工之妙的,当我们找不到路径时,也是通过回溯到之前的路径重新搜索的。当然也可以用递归来解决这个问题,不过递归也是栈结构的一种应用。具体算法思路见伪代码,该方法时间复杂度 O(n2)。
伪代码
代码实现
#include<iostream>
#include<vector>
using namespace std;
bool judgePlacement(int top, vector<int>& queens_stack);
int main()
{
int num;
cin >> num;
vector<int> queens_stack(num + 1); //构造函数初始化
int top = 1; //栈顶指针
while (top)
{
queens_stack[top]++; //栈顶表示的位置横向移动到下一行
while (queens_stack[top] <= num && judgePlacement(top, queens_stack) == false)
{
queens_stack[top]++; //在 top 同一行的后面搜索可放置的列
}
if (queens_stack[top] <= num)
{
if (num == top) //棋盘搜索完毕,得到一组解
{
for (int i = 1; i <= num; i++) //输出棋盘
{
for (int j = 1; j <= num; j++)
{
if (j == queens_stack[i])
cout << "Q ";
else
cout << "* ";
}
cout << endl;
}
cout << endl;
}
else //棋盘没有搜索到最后一行,下一行入栈
queens_stack[++top] = 0;
}
else //在同一行中找不到任何一列可以放置
top--; //退栈,回溯到上一行搜索可放置的列
}
return 0;
}
bool judgePlacement(int top, vector<int>& queens_stack)
{
for (int i = 1; i < top; i++) //判断当前位置是否与之前的每一行中的棋子处于同列或同斜向
{
if ((queens_stack[i] == queens_stack[top])
|| (fabs(queens_stack[i] - queens_stack[top]) == fabs(i - top)))
return false;
}
return true; //说明该位置可放置
}
运行效果