问题:
在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
思路:
对于计算机问题的求解,我们完全可以模仿人类的解题思路,并将其抽象化,形成可供计算机执行的代码。
首先,我们考虑使用枚举的方式,但应保证枚举不重复。故我们将八个棋子分别编号,并让编号小的棋子永远在编号大的棋子前,从左到右,从上到下,从编号小的棋子依次排列到编号大的棋子。
按照这个思路,我们可以定义一个8*8的二维数组,二维数组的值代表这个棋子的编号。我们先将第一个棋子放置于0*0的位置,然后距离最近可能的第二个棋子的位置,即1*3,依次类推。这种方法很像一个循环,不过普通的循环显然无法实现这个方案。因此我们参考递归的实现思路,定义一个函数,用来处理这个问题。
当按照这个思路做完,我们会发现没有一个结果符合我们的要求。因此我们回去寻找问题。很容易发现,第七个棋子并不一定放在离第六个棋子最近可能的地方,同理,第二个棋子也不一定放在离第一个棋子最近可能的地方(即位置1*3)。因此,我们要重新对上一个棋子定义位置,直到第一个棋子到达最后的位置,我们才做完了所有可能。
对问题进一步抽象,我们假设对第n个棋子进行操作,在定下第n个棋子的一个位置后再定第n+1个棋子,同时我们还应重新定第n个棋子的位置。至此,我们就能解决这个问题。
代码:
#include<stdio.h>
int find(int m, int n, int count);
int fight(int m, int n);
void show();
int queen[8][8] = { 0 }; //数字下标为棋盘位置,数值为第几个棋子,0代表无棋子
int methods = 0; //统计总共有多少种情况
int main() {
find(0, 0, 1);
printf("总共有:%d", methods);
return 0;
}
//从m行 n列寻找 第count个
int find(int m, int n,int count) {
if (n == 8) {
m++;
n = 0;
}
int q; //q为当前列
for (q = n; q < 8; q++) {
if (fight(m, q) == 0) {
queen[m][q] = count;
goto NEXT; //计算下一个棋子
}
}
for (m++; m < 8; m++) {
for (q = 0; q < 8; q++) {
if (fight(m, q) == 0) {
queen[m][q] = count;
goto NEXT;
}
}
}
return 0; //查找到最后一个都没找到,则停止查找
NEXT:
if (count == 8) {
show();
methods++;
}
if (count < 8) {
find(m, q+1, count+1); //向后查找
}
queen[m][q] = 0; //重新查找当前棋子
find(m, q+1, count);
}
//判断是否会相互攻击 攻击返回1 不会攻击返回0
int fight(int m, int n) {
for (int q = 0; q < 8; q++) {
if (queen[m][q] > 0)
return 1;
}
for (int q = 0; q < 8; q++) {
if (queen[q][n] > 0)
return 1;
}
for (int a = m, b = n; a<=7&&a>=0&&b<=7&&b>=0; a++, b++) {
if (queen[a][b] > 0)
return 1;
}
for (int a = m, b = n; a <= 7 && a >= 0 && b <= 7 && b >= 0; a++, b--) {
if (queen[a][b] > 0)
return 1;
}
for (int a = m, b = n; a <= 7 && a >= 0 && b <= 7 && b >= 0; a--, b++) {
if (queen[a][b] > 0)
return 1;
}
for (int a = m, b = n; a <= 7 && a >= 0 && b <= 7 && b >= 0; a--, b--) {
if (queen[a][b] > 0)
return 1;
}
return 0;
}
//将当前棋盘展示
void show() {
for (int m = 0; m < 8; m++) {
for (int n = 0; n < 8; n++) {
printf("%d ", queen[m][n]);
}
printf("\n");
}
printf("\n");
}
总结(回溯算法):
本问题的解题思路使用的是典型的回溯算法。我们可以将该算法思路类比为走迷宫。我们可以先在所有岔路口上左转,一直走到路的尽头。若此方法无法走到终点,我们回到上一个选择点,选择一条不同的路,依此类推。这种方法可以在不出现重复的情况下走完所有的路,并在计算机中可以很好的通过函数嵌套的方式( 子问题与原始问题为同样的事)实现。
拓展:
数独问题
另:
基于C++,使用位运算的版本(具有更高的计算效率和更低的内存占用量),支持棋盘大小<32*32的解法(思路可参考力扣 N皇后问题 官方解法):
#include <iostream>
class Solution
{
public:
// n为棋盘大小
int totalNQueens(int n)
{
n_ = n;
return putQueen(0, 0, 0, 0);
}
// 若不能放置,对应bit位置为1, 否则为0
int putQueen(int row, unsigned int column, unsigned int diagonal_lr, unsigned int diagonal_rl)
{
if (row == n_)
{
return 1;
}
else
{
int answer_num = 0;
unsigned int choice = column | diagonal_lr | diagonal_rl;
for (int i = 0; i < n_; i++)
{
if (choice % 2 == 0)
{
answer_num += putQueen(row + 1, column | (1 << i),
(diagonal_lr | (1 << i)) >> 1,
(diagonal_rl | (1 << i)) << 1);
}
choice >>= 1;
}
return answer_num;
}
}
int n_;
};
int main()
{
Solution solution;
std::cout << solution.totalNQueens(4) << std::endl; // 2
// 8皇后问题
std::cout << solution.totalNQueens(8) << std::endl; // 92
std::cout << solution.totalNQueens(9) << std::endl; // 352
}