1.问题引入
n-皇后问题是指将 n 个皇后放在 n∗n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。现在给定整数n,请你输出所有的满足条件的棋子摆法。
例如下面是一个八皇后的摆法
2.万丈高楼平地起
首先先了解一个全排列问题。我们把1~n这n个整数按某个顺序摆放的结果称为这n个整数的排列。全排列指的是这n个整数能形成的所有排列。
例如1、2、3这3个数字全排列为(1,2,3) (1,3,2) (2,1,3) (2,3,1) (3,1,2)(3,2,1)。现在要求实现按字典序从小到大的顺序输出1~n的全排列,
字典序:(a1,a2,a3…an)字典序小于(b1,b2,b3…bn)是指存在一个i使得a1=b1、a2=b2…a(i-1)=b(i-1)、ai<bi成立。前面的1~3的全排列就是按字典顺序从小到大给出的。
那么怎么求出1~4的全排列呢?
1.全排列思路
小白思路:
for(4次){
for(3次){for(2次){确定序列} }
}
大佬思想:
采用分治和递归的思想:参考之前的分治和递归
例如将输出1~4的全排列问题划分为:
- 输出1开头的全排列
- 输出2开头的全排列
- 输出3开头的全排列
- 输出4开头的全排列
划分为了4个类似的子问题即从第一个位置开始的全排列。
其中上述任意一个子问题又可以再划分为多个从第2个位置开始的全排列。
第2个位置开始的全排列又可以划分成多个从第3个位置开始的全排列。
用树状图理解会更形象
最后合并所有解决子问题的序列,得出全排序
通过上述分析我们可以得到子问题:从第x位置开始的全排列。
x取n时则是最后一次划分。
由此我们可以得到:
递归式:generate(int index);
递归边界:index=n+1;
下面用伪代码表示
void generate(int index){
if(index==n+1){输出序列;}
...
generate(index++);
...
}
2.预处理
无论是哪种思路都逃不过一个问题,就是我们在某个位置填入数字时如何保证填入的数字之前没有填入过?
当然可以遍历数组来判断,不过这种方法耗时太多。这里可以采用散列的思想,预处理定义一个hash数组。详细请参考散列
3.解决全排列问题
#include <cstdio>
int n;
const int max=100;
int result[max];
bool hashtable[max]={false};
void generate(int index);
int main(){
scanf("%d",&n);
generate(1);
return 0;
}
void generate(int index){
if(index==n+1){//递归边界
for(int i=1;i<=n;i++){
printf("%d",result[i]);//输出序列
}
printf("\n");
}
for(int x=1;x<=n;x++){//枚举填数
if(hashtable[x]==false){
result[index]=x;
hashtable[x]=true;
generate(index+1);
hashtable[x]=false;//处理完子问题后还原状态,注意理解
}
}
}
注意要理解:每次递归结束hashtable[x]=false;
例如请问上述代码运行完结果后,hashtable中的1~n位置是为true还是为false?
事实上:仍然为false
2.n皇后问题的解决
对于n皇后问题,如果采取组合数的方式来枚举每一种情况则要从nn的位置中选择n个位置,则需要Cnn(n)枚举量。
如果我们考虑到每行放一个皇后,每列放一个皇后。将n列皇后写出则可以得到一个长度为n的数列。
例如4213 数字的位置序号代表该皇后的列,数字的大小代表该皇后所在的行。
于是,n皇后问题便可以借鉴全排序来解决。但由于规定任意2个皇后均不在同一对角线上,所以可以添加一个判断函数来输出。
bool NonDiagona(int result[]){
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
if(abs(i-j)==abs(result[i]-result[j])){
return false;
}
}
}
return true;
}
测试代码如下:
#include <cstdio>
#include <cmath>
int n;
const int max=100;
int result[max];
bool hashtable[max]={false};
bool NonDiagona(int result[]);
void generate(int index);
int main(){
scanf("%d",&n);
generate(1);
return 0;
}
void generate(int index){
if(index==n+1){//递归边界
if(NonDiagona(result)){
for(int i=1;i<=n;i++){
printf("%d",result[i]);//输出序列
}
printf("\n");
}
}
for(int x=1;x<=n;x++){//枚举填数
if(hashtable[x]==false){
result[index]=x;
hashtable[x]=true;
generate(index+1);
hashtable[x]=false;//处理完子问题后还原状态,注意理解
}
}
}
bool NonDiagona(int result[]){
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
if(abs(i-j)==abs(result[i]-result[j])){
return false;
}
}
}
return true;
}
3.回溯法优化n皇后
在上述的递归中,我们可以发现当已经放置一部分皇后的时候,可能无论剩下的皇后怎么排都不合法。例如183… 此时就已经不合法了没必要递归下去,直接返回上层就可,这样就省出了很多计算量。
回溯法:当达到递归的某一层时,由于一些事实不需要任何子问题递归,就可以直接返回上一层。
只需要在全序列代码中填入数字时就判断函数就可以完成优化
判断函数:
/*
index:要填入的数的位置
x:要填的数
*/
bool NonDiagona(int result[],int index,int x){
for(int pre=1;pre<index;pre++){
if(abs(index-pre)==abs(x-result[pre])){
return false;
}
}
return true;
}
全部测试代码如下:
#include <cstdio>
#include <cmath>
int n;
const int max=100;
int result[max];
bool hashtable[max]={false};
bool NonDiagona(int result[],int index,int x);
void generate(int index);
int main(){
scanf("%d",&n);
generate(1);
return 0;
}
void generate(int index){
if(index==n+1){//递归边界
{
for(int i=1;i<=n;i++){
printf("%d",result[i]);//输出序列
}
printf("\n");
}
}
for(int x=1;x<=n;x++){//枚举填数
if(hashtable[x]==false){
if(NonDiagona(result,index,x)){
result[index]=x;
hashtable[x]=true;
generate(index+1);
hashtable[x]=false;
}
//处理完子问题后还原状态,注意理解
}
}
}
/*
index:要填入的数的位置
x:要填的数字
*/
bool NonDiagona(int result[],int index,int x){
for(int pre=1;pre<index;pre++){
if(abs(index-pre)==abs(x-result[pre])){
return false;
}
}
return true;
}
输入:4
输出:
2413
3142