这篇博客是记录自己刷算法的过程,分享一下对N皇后问题的看法~
问题
根据国际象棋的规则,皇后可以攻击与同处一行、一列或一条对角线上的任何棋子。给定 n 个皇后和一个 m×m大小的棋盘,寻找使得所有皇后之间无法相互攻击的摆放方案。
这里以北大复试上机题为例,分享一下学习心得~
求解
这道题本质上是搜索所有的解空间,并寻找记录满足要求的可能解,并根据题意按要求输出第i个解。这也正是回溯算法。
说个题外话,回溯算法跟深度优先搜索有什么区别呢?
可以认为回溯算法=dfs+剪枝,也就是说dfs是以深度优先的方式,遍历整个解空间。而回溯算法的搜索方式正是与dfs一样,只不过我们可以添加必要的判定条件,使dfs在满足该判定条件时可以进行不必要的搜索,这也就是所谓的剪枝。
回到该问题,具体思路是按顺序一行一行的尝试求解问题的解,以所在行为例,挨个尝试每一列,并判断当前位置是否满足要求。
我们的要求是,
- 所在的行没有别的棋子,因为我们是按行搜索,所以我们当前所在的行一定就只有我们一个棋子,所以我们并不需要做啥判断。
- 所在的列没有别的棋子,这个点很好解决,我们只要申请一个长度与列数相等的数组记录该列上是否有棋子即可。
- 所在的主对角线上没有别的棋子。
- 所在的副对角线上没有别的棋子。
主副对角线上的棋子,我们该怎么判断呢,我相信很多人遇到这个问题都很棘手,这也可能是本题中最大的难点。
我们先随便确定一个坐标,看看其主对角线上,副对角线上有啥规律可以发现?
我们不难发现,所有的主对角线上(黑色字体)的元素行坐标减去列坐标都相等,副对角线上(红色字体)所有的元素的行坐标加列坐标都相等。这规律是永远成立的,不信的大伙可以多试试几个~
那么现在的问题就变成了主对角线上相减的结果,副对角线上相加的结果是否唯一呢?
答案是唯一的,因为任意一条对角线上的坐标是唯一的,其差或者和,也一定是唯一的。这样一来,我们就可以对对角线进行标号区分了。
由于两坐标相减的最小数是1-n(数组下标就是0-(n-1)),最大值是n-1(数组下标就是n-1-0),所以我们需要对相减的对角线标号进行偏移到[0-2n-2],也就是diag_m=row-col+n-1(diagonal_main:主对角线,n是nxn的棋盘)
标完号的效果大概如下所示:
代码
于是上面那道算法题的代码如下:
#include <iostream>
#include <vector>
using namespace std;
vector<string> ans_arr;
vector<bool> col_arr(8,true); //因为按行遍历,不可能出现同行,所以不需要记录行的
vector<bool> diag_m(15,true); //diagonal_main记录主对角线上是否是空位15=2*8-1
vector<bool> diag_s(15,true); //diagonal_sub记录副对角线上是否是空位15=2*8-1
void dfs(string ans,int row) {
// 是问题解则记录
if(ans.size()==8){
ans_arr.push_back(ans);
return;
}
for(int col=0;col<8;++col){
int m=row-col+7;//计算当前坐标位于主对角线上第几条
int s=row+col;// 计算当前坐标位于副对角线上第几条
// 行,列,主副对角线均没有皇后
if(col_arr[col]&&diag_m[m]&&diag_s[s]){
// 标记改列,主副对角线都已访问
col_arr[col]=false;
diag_m[m]=false;
diag_s[s]=false;
// 记录解
ans+=col+'1';
// 进行下一行判断
dfs(ans, row+1);
// 回溯
ans.erase(ans.size()-1);
col_arr[col]=true;
diag_m[m]=true;
diag_s[s]=true;
}
}
}
int main() {
string ans;
dfs(ans,0);
int n;
while (cin >> n) {
cout << ans_arr[n - 1] << endl;
}
return 0;
}