生物专业的小希前来报道!
时隔两个多月我又回来啦,一边犯懒一边觉得没有什么值得我写文章的题目,直到遇见了这个皇后问题!虽然是蓝桥杯的基础问题但我觉得好难啊= = 业余的我当然是想不出来怎么做的,找了好几个代码觉得其中一个最好理解且最为简洁~(如果我都能理解相信很多人也可以)
下面是我自己对这个题目和算法的解读(基于菜鸟视角:)
问题描述
给定一个n*n的棋盘,棋盘中有一些位置不能放皇后。现在要向棋盘中放入n个黑皇后和n个白皇后,使任意的两个黑皇后都不在同一行、同一列或同一条对角线上,任意的两个白皇后都不在同一行、同一列或同一条对角线上。问总共有多少种放法?n小于等于8。
输入格式
输入的第一行为一个整数n,表示棋盘的大小。
接下来n行,每行n个0或1的整数,如果一个整数为1,表示对应的位置可以放皇后,如果一个整数为0,表示对应的位置不可以放皇后。
输出格式
输出一个整数,表示总共有多少种放法。
这个问题是基于【八皇后问题】进行的升华和改编,在八皇后问题中,棋盘被给定为8×8的大小,没有黑皇后和白皇后之分,只有一种皇后且同样是放入n个,默认棋盘的每个位置都可以放皇后。
我们先来解决八皇后问题~
Important Point
① 抛弃用二维数组表示棋盘的方法,采用一维数组表示棋盘。
int chess[row]; //row代表行数,数组元素里面存放的值表示列数
我们知道一共要摆放n个皇后,且她们互不同行,所以很容易想到的基本条件是每行有且只有一个皇后,所以这个一维数组实际上表示的是皇后的位置,如chess[1] = 3
表示第1行的皇后在第3列。
② 采用递归的思想。我们定义的数组下标表示的是行数,所以一行一行地放皇后。(从row = 0
一层一层地递归到row = 7
)
void arrayQueen(int row) {
if(row == 8) {
ans++;
return;
}
for(int column = 0; column < 8; column++) {
if(check(row, column)) {
chess[row] = column;
arrayQueen(row + 1);
chess[row] = -1;
}
}
}
代码解释
row > 7
时,所有行数的皇后都已经放置成功,说明找到了一种放置方案,结束该行的放置程序,回到上一行arrayQueen
的下一个循环(即下一个column
位置),或者回到上上一行arrayQueen
的下一个循环,重新找下一种放置方案。- 在特定的行里面要一个一个地去检查每个位置合不合格,即
for(int column = 0; column < 7; column++)
check
函数,返回真表示位置合格,先记录一下第row
行的皇后在第column
列,接着就可以去下一行放皇后啦。- 当程序跳到下一行并找到了合格的位置时(即check函数为真时),程序就会跳到下下一行继续寻找;但如果下一行没有一个位置合格的时候,相当于
arrayQueen(row + 1)
白白执行了一遍,那么就会回到这一行的arr[row] = -1
,意思是虽然column
取定的这个位置在这一行合格了,但对于下一行来说它不合格,所以要么程序就column++
跳到下一个循环,要么就找不到任何合格位置,回到arrayQueen(row - 1)
chess[row] = column
记录这一行的皇后的位置是因为我们已经假设皇后摆好了(虽然它对于下一行来说有可能不合格),记录位置也是有利于下一行检查位置是否合格时与前面行数的位置进行比对,后面的chess[row] = -1
是清空之前记录此位置的一个操作。
③ check函数,检查这个位置是否合格。
bool check(int row, int column) {
for(int i = 0; i < row; i++)
if(chess[i] == column)
return false;
if(abs(i-row) == abs(chess[i]-column))
return false;
return true;
}
有两种情况表示这个位置不合格:
- 检查前面已放置的皇后,列数相同时不合格(行数不会相同,因为我们是一行一行放的)
- 检查前面已放置的皇后,位于同一个对角线时不合格(对角线的含义:行数之差的绝对值等于列数之差的绝对值)
看到这里就先去自己写一下八皇后问题的代码吧!答案是92,检验一下看看自己写的对不对,笔芯 ღ( ´・ᴗ・` )
接下来放【2n皇后问题】的解决方案!
Important Point
① 设置一个二维数组,用于表示哪些位置可放皇后,哪些位置不可放。
int arr[8][8];
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
cin >> arr[i][j];
这个数组也就是题目中要求输入的数组,设置完成后,在皇后的check
函数里面也要加一条判断语句——
if(arr[row][column] == 0)
return false;
同时,这也给黑皇后和白皇后的重合问题提供了解决的便利。
当白皇后放好后,黑皇后就不能放在这个位置,可将此位置的arr[row][column] = 0
这样在检查黑皇后的位置是否合格时,既检查了初始输入的数组元素是否为1,也检查了这个位置是否已经放了白皇后。下面是白皇后的放置函数——
for(int column = 0; column < n; column++)
if(checkWhite(row, column)) {
chessWhite[row] = column;
arr[row][column] = 0; //白皇后占了位置
arrayWhite(row + 1);
chessWhite[row] = -1; //跳到下一个循环(下一列)或上一行
arr[row][column] = 1;
}
其中第3-4行是假设白皇后已经放好的情况,此时对于这一行而言,这个位置是合格的。接下来跳到下一行,若对于下一行来说,该位置不合格(下一行的for循环白白执行了一遍),则跳回此行的arrayWhite(row)
函数,换下一个位置前将之前的设定清空,也就是第6-7行,说明这一行的白皇后暂时还没有找到位置,arr[row][column]
变回1,即没有被白皇后占位。
② 从白皇后到黑皇后的过渡。
在我的代码中,我打算先放白皇后再放黑皇后。在白皇后的放置算法里面当row == n
时,说明全部的白皇后已经放置完毕,这时候应该开始放黑皇后。
if(row == n) {
arrayBlack(0);
return;
}
执行完arrayBlack(0);
也就是说,在这个确定的(n个)白皇后的排列下,所有(n个)黑皇后可以排列的情况都已经寻找完毕,这个时候我们return;
回到白皇后排列的最后一行,改变它的位置(或者是回到倒数第二行等),也就是重新改变白皇后的排列,直到寻找到白皇后的下一个满足条件的排列后,又开始排列黑皇后。
下面是完整的代码实现~
代码实现
#include <iostream>
#include <cmath>
using namespace std;
int ans = 0, n;
int chessWhite[8], chessBlack[8], arr[8][8];
void arrayWhite(int row);
void arrayBlack(int row);
bool checkWhite(int row, int column);
bool checkBlack(int row, int column); //声明
int main()
{
cin >> n;
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
cin >> arr[i][j];
for(int i = 0; i < n; i++) //位置初始化
{
chessWhite[i] = -1;
chessBlack[i] = -1;
}
arrayWhite(0); //放置白皇后
cout << ans << endl;
return 0;
}
void arrayWhite(int row)
{
if(row == n) {
arrayBlack(0); //白皇后放好 去放黑皇后
return;
}
for(int column = 0; column < n; column++)
if(checkWhite(row, column)) {
chessWhite[row] = column;
arr[row][column] = 0; //白皇后占了位置
arrayWhite(row + 1);
chessWhite[row] = -1; //跳到下一个循环(下一列)或上一行
arr[row][column] = 1;
}
}
void arrayBlack(int row)
{
if(row == n) {
ans++;
return;
}
for(int column = 0; column < n; column++)
if(checkBlack(row, column)) {
chessBlack[row] = column;
arrayBlack(row + 1);
chessBlack[row] = -1; //跳到下一个循环(下一列)或上一行
}
}
bool checkWhite(int row, int column)
{
if(arr[row][column] == 0)
return false;
for(int i = 0; i < row; i++) {
if(chessWhite[i] == column)
return false;
if(abs(chessWhite[i]-column) == abs(i-row))
return false;
}
return true;
}
bool checkBlack(int row, int column)
{
if(arr[row][column] == 0)
return false;
for(int i = 0; i < row; i++) {
if(chessBlack[i] == column)
return false;
if(abs(chessBlack[i]-column) == abs(i-row))
return false;
}
return true;
}
算法解读
在寻找2n皇后问题的解答的过程中了解到这种算法叫【搜索】
具体而言,此题的解决方案应该属于深度优先搜索。可惜无论是百度百科,还是别人的CSDN博客,我都没看懂这个算法到底是干嘛。作为一个连“栈”是什么都不知道的小白菜,我做了一个示意图来便于大家理解(基于此题哦)
如图所示,row = i - 1
的位置确定了,程序才会跳到row = i
此时chessWhite(i - 1)
已经确定了,但如果row = i
的位置中都没有合格的,此时程序跳回row = i - 1
,这个时候原本确定的位置也不成立了,所以chessWhite(i - 1)
要重新初始化为-1。
之所以说它是深度优先搜索,是因为每一次排列,如果某个位置合格就会跳到下一行,直到某一行没有位置合格或者跳到最后一行(证明找到一种放置方案),这时程序也只会回到上一行改变一个位置重新寻找,而不是回到最初的地方从头开始,有一种“我先把这条路摸透,看看它对不对,不对的话回到上个岔路口继续尝试,而不是回到起点”的感觉。
每个位置都是一个岔路口,都有n条路可以走,分别对应下一行的n个位置。第一行有n个位置,即n种可能,每种可能对应第二行的n个位置,一共就是n×n种可能。考虑完所有的行数,有n×n×…×n(n个n相乘)种可能,搜索算法就考虑了这些可能。某些路口是不符合要求的,于是就没有往下走,而是跳到与之平行的另一个路口;如果和它平行的所有路口都不符合要求,那么就回到上一行平行的一系列路口。
讲了这么多废话终于讲完啦,如果有错误的地方欢迎指正~ 不过管它错不错呢,反正这道题历经千辛万苦我还是做出来了,开心!
不知道还能不能等到下次打卡的时候了,写博客真累,大家都加油哇🤩