n皇后问题
思路:
根据上一篇全排列的递归思想,我们接下来讲解n皇后问题,n皇后问题是指在一个n*n的国际象棋棋盘上放置n个皇后,时得这n个皇后两辆均不在同一列、同一行、同一对角线上,求合法方案。例如下图是n=8的情况,图中两两皇后之间符合上述的条件,所以它是一个合法的方案。
对于这个额问题,如果采用组合数的方法来枚举每一种情况(即从n²个位置中选择n个位置),那么将需要C(n²,n)=n²!/((n²-n)!*n!)的枚举量,当n=8时就是54 502 232次枚举,如果n更大,那么就无法承受。
但是换个思路,考虑到每行只能防止一个皇后,每列也只能防止一个皇后,那么如果把n列皇后所在的行号依次写出,那么就会是1~ n的所有排序。对于上面图片的排序就是61528374和36271485。于是只需要枚举1~n的所有排序,查看每个排序对应的位置方案是否合法,统计其中合法的方案即可。由于总共有n!个排序,因此当n=8时只需要40320次枚举,比之前的做法优秀许多。
于是可以在全排列的代码基础上进行求解。由于当到达递归边界时表示生成一个排序,所以需要在其内部判断是否是合法方案,即遍历每两个皇后你,判断他们是否在同一对角线上(不在同一列和同一行是显然的),若不是,则累计计数变量count即可。代码如下,当n=8时count最后等于92。
代码:
#include<iostream>
using namespace std;
const int maxn=11;
int n,P[maxn],hashTable[maxn]={false};
int conut=0;
void generateP(int index)
{
if(index==n+1) //递归边界,生成一个排序
{
bool flag=true; //flag为true表示当前是一个合法的方案
for(int i=1;i<=n;i++) //遍历任意两个皇后
for(int j=i+1;j<=n;j++)
if(abs(i-j)==abs(P[i]-P[j])) //如果在一条对角线上
flag=false; //不合法
if(flag)
conut++; //若当前方案合法
}
for(int x=1;x<=n;x++) //和全排列一样
{
if(hashTable[x]==false)
{
P[index]=x; //令p的第index位为x,即把x加入当前排序
hashTable[x]=true; //记x已经在p中
generateP(index+1);
//处理排序的第index+1号位
hashTable[x]=false; //已经处理玩P[index]为x的子问题,还原状态
}
}
}
int main()
{
n=8; //8皇后
generateP(1); //从1开始选
cout<<conut<<endl;
return 0;
}
运行结果如下:
92
优化:
上面这种枚举所有情况,然后判断每一种情况是否合法的做法是非常朴素的(因此一般把不使用优化算法、直接用朴素算法来解决问题的做法称为暴力法)。事实上,通过思考可以发现,当已经放置了一部分皇后时(对应于生成了排列的一部分),可能剩余的皇后无论怎样放置都不可能合法,此时就没必要往下递归了,直接返回上层即可,这样可以减少很多计算量。
一般来说,如果在到达递归边界前的某层,由于一些事实导致已经不需要往任何一个子问题递归,就可以直接返回上一层。一般把这种做法称为回溯法。
下面的代码都采用了回溯的写法,请读者体会它与上面代码的区别:
代码:
void generateP1(int index)
{
if(index==n+1) //递归边界,生成一个合法方案
{
conut++; //能达到这里的一定合法
return;
}
for(int x=1;x<=n;x++) //第x行
{
if(hashTable[x] ==false) //第x行还没有皇后
{
bool flag=true;//flag为true表示当前皇后不会和之前的皇后冲突
for(int pre=1;pre<index;pre++) //遍历之前的皇后
{
//第index列皇后的行号为x,第pre列皇后的行号为P[pre]
if(abs(index-pre)==abs(x-P[pre]))
{//可以记为(列-列==行-行)
flag=false;
break;
}
}
if(flag)
{
P[index]=x; //令第index列皇后的行号为x
hashTable[x]=true; //记x已经在p中
generateP1(index+1); //处理排序的第index+1号位
hashTable[x]=false;
}
}
}
}