问题表述
在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。是回溯算法的典型案例。
全排列
要解决n皇后问题,首先需要了解一下全排列问题。在algori头文件中有函数next_permutation()可以用于生成全排列,严格说来只是可以用来生成下一个排列,如下。
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int num[3]={1,2,3};
do{
printf("%d %d %d\n",num[0],num[1],num[2]);
}while(next_permutation(num,num+3));
return 0;
}
运行结果
这段代码使用了do{ …}while()循环。其中next_permutation()函数在到达全排列的最后一个时,会返回false,用于推出循环。
实现全排列
#include <iostream>
#include <vector>
#include <algorithm>
#include <unordered_map>
using namespace std;
int n=3;//数的个数
vector<int> temp;//用于记录排列
unordered_map<int,bool> vis;//用于记录当前数是否已在排列中
void show()
{
for(int i=0;i<temp.size();i++){
if(i) printf(" ");
printf("%d",temp[i]);
}
printf("\n");
}
void solve(int cnt)
{
if(cnt==n+1){//temp中保存了一个完整的排列
show();//输出
return;
}
for(int x=1;x<=n;x++){
if(!vis[x]){//如果x尚未在排列中
vis[x]=1;
temp.push_back(x);
solve(cnt+1);
vis[x]=0;
temp.pop_back();
}
}
}
int main()
{
solve(1);
return 0;
}
在一些递归问题中,有时需要记录路径上的权值,则使用vector向量最好不过了,因为在当前层递归结束之后,下层回溯上来,直接使用vector的pop_back()函数,可以直接将当前的向量中的最后一个元素删掉,这在一些树的路径问题中非常常用。
n皇后问题
n皇后问题限定了皇后个数为8个,也就是,计算量不大。可以使用暴力方法进行模拟。但是从8*8的棋盘上选出8个位置来,之后再进行有效性判断,计算量也是非常大。
转换一下思路,n皇后问题就变成了全排列问题。
合理的解决方案是,8个皇后两两之间不在同一行、同一列、同一对角线上。
回到上面的图片上,对棋盘从左上角开始给行、列编号,从1开始,然后发现,第1列皇后在第4行上,第2列皇后在第2行上,,,,第8列皇后在第6行上。当将8个皇后按照列数从1到8的顺序将各自的行号单独拿出来时,本图中即为4、2、8、5、7、1、3、6,会发现,这是一个1–8的排列。但是并不是说1-8的所有排列都是符合要求的结果,因为,排列只能保证不存在两个皇后不在同一行、同一列,而无法保证不在同一个对角线上,但是至已经大大地减小了计算量。
每当生成一个排列时,对当前排列进行对角线检验,若是合格则生成一个方案。代码如下
#include <iostream>
#include <vector>
#include <algorithm>
#include <unordered_map>
#include <ctime>
using namespace std;
int n=8;//皇后的个数
int ans=0;
vector<int> temp;//用于记录排列
unordered_map<int,bool> vis;//用于记录当前数是否已在排列中
void solve(int cnt)
{
if(cnt==n+1){//temp中保存了一个完整的排列
for(int i=0;i<temp.size();i++){
for(int j=i+1;j<temp.size();j++){
if(abs(i-j)==abs(temp[i]-temp[j])) return;
}
}
ans++;
return;
}
for(int x=1;x<=n;x++){
if(!vis[x]){//如果x尚未在排列中
vis[x]=1;
temp.push_back(x);
solve(cnt+1);
vis[x]=0;
temp.pop_back();
}
}
}
int main()
{
int start=clock();
solve(1);
printf("用时%d毫秒\n",clock()-start);
printf("8皇后的答案数量是:%d",ans);
return 0;
}
运行结果为
可以发现代码只是在全排列代码基础之上进行了一点改造,去掉了输出排列的函数,每当生成一个排列之后,进行有效性检验,即检验是否有两个皇后在同一个对角线上,如果是,直接返回上一层,如果有效,则另ans++;
外加了一个时间函数,记录了找到n皇后问题所用时间。可以发现,用时为79毫秒。
稍微优化
上面代码中,是每当生成一个全排列再去检验是否有两个皇后位于同一个对角线上,其实有的排列,并不用等到完全生成之后再去检验对角线的问题。
在生成排列的过程中去检验即可,如果当前的不完整的排列中,没有两个皇后在同一个对角线上,再进入下一层,代码如下
#include <iostream>
#include <vector>
#include <algorithm>
#include <unordered_map>
#include <ctime>
using namespace std;
int n=8;//皇后的个数
int ans=0;
vector<int> temp;//用于记录排列
unordered_map<int,bool> vis;//用于记录当前数是否已在排列中
void solve(int cnt)
{
if(cnt==n+1){//temp中保存了一个完整的排列
ans++;//能到达此处,一定是一个解
return;
}
for(int x=1;x<=n;x++){
if(!vis[x]){//如果x尚未在排列中,此处以下为关键语句
bool tag=true;
for(int i=0;i<temp.size();i++){//稍微解释以下,因为x还未在排列中出现,若是x可以进入
if(abs(temp.size()-i)==abs(x-temp[i])){//排列,则肯定去往当前temp.size()位置
tag=false;//需要判断当前temp中已有的皇后是否会与即将放在temp.size()位置上的
break;//x是否冲突
}
}
if(tag){
vis[x]=1;
temp.push_back(x);
solve(cnt+1);
vis[x]=0;
temp.pop_back();
}
}
}
}
int main()
{
int start=clock();
solve(1);
printf("用时%d毫秒\n",clock()-start);
printf("8皇后的答案数量是:%d",ans);
return 0;
}
运行结果如下:
这次运行时间变为了9毫秒,快了很多
java版
/**
* ClassName: NQuenes
* PackageName: leetcode.editor.cn.doc.content
* @author: Joshua Lee
* @create: 2023/10/20 - 11:26
* @description:
*/
// 用来生成n皇后的解
public class NQueens {
private static int[] pos = null; // pos[col] = row:表示第 col 列的皇后放在了第 row 行
private static char[][] board = null; // 存放n个皇后的棋盘
private static boolean[] used = null; // used[row] = true:第 row 行 已经放了皇后了
private static int cnt = 0; // 记录一共有多少种答案
private static void dfs(int curCol, int n) {
// 如果当前 curCol 为n,即要往第n列上放皇后,就说明,此时找到了一个结果
if (curCol == n) {
System.out.println("第" + (++cnt) + "种答案是:");
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.print(board[i][j] + " ");
}
System.out.println();
}
System.out.println();
return;
}
// 要往curCol列上放数据,试探当前哪一行还没有皇后
for (int row = 0; row < n; row++) {
// 如果row行还没有放皇后
if (!used[row]) {
// 先试探,将curCol列的皇后放在row行上,会不会与之前已经放置的皇后冲突
boolean flag = true;
for (int preCol = 0; preCol < curCol; preCol++) {
// 如果curCol列上的皇后放在row行上,和之前的某个皇后在同一条对角线上,则冲突了。
// 使用这种排列的方式,皇后们不在 同一行、同一列是自动保证的
if (curCol - preCol == Math.abs(row - pos[preCol])) {
flag = false;
break;
}
}
// 如果没和前面的发生冲突
if (flag) {
// 将 curCol 列的皇后放在 row 行
board[curCol][row] = 'Q';
// 记录 curCol 列的皇后在 row 行
pos[curCol] = row;
// 设置 row 行已经有皇后了
used[row] = true;
// 递归到下一次,为 下一列 选择一个皇后
dfs(curCol + 1, n);
// 下层回溯回来,要在本列尝试别的行上放数据
// 必须将used[row]设为false。因为确实row行没有皇后
used[row] = false;
// board[curCol][row]设为'.'。因为否则就真的改了board中的数据了,因为board是全局变量
board[curCol][row] = '.';
// pos[curCol] 不用更改,因为还在同一层,就是要更改这个数据
}
}
}
}
public static void main(String[] args) {
int n = 9;
pos = new int[n];
used = new boolean[n];
board = new char[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
board[i][j] = '.';
}
}
long startTime = System.currentTimeMillis();
dfs(0, n);
long endTime = System.currentTimeMillis();
System.out.println("耗时:" + (endTime - startTime) + "毫秒");
}
}