八皇后问题,不是很难但是,由于在一个点上理解不到位,导致花了数天的时间才解出来,可以说是相当难受了,正因为难受,所以才更要把它写出来。写出自己错误的原因,并且理清楚其中原由对自己以后的成长有莫大帮助。
(ps:如果你也有类似的想了好久的问题,然后最终通过自己的努力解决掉了它,首先恭喜你解决掉它,这其中的喜悦估计只有本人才能理解了,但是仅仅是做出来是远远不够的,我建议你以文字的形式把它记录下来。因为如果只是跑出这个程序而不去反思自己出错的原因的话,恭喜你,这道题白做了)
好了,接下来写一下我犯错的原因和分析出错误的过程吧。
注:如果只是想看正确的代码,请略过这部分。
首先我的思想很简单,就是通过回溯的方法去一个个判断每一个点是否可以放置一个皇后,然后通过限界函数进行剪枝,可大大减少该算法的时间复杂度。
先放上错误代码:
#include<iostream>
#include<cmath>
using namespace std;
int all[1000][10]={0};
int tmp[8]={0};
int n=0;
void queens(int r){ //r代表第r行
if(r==8){
for(int i=0;i<8;i++)
{
all[n][i]=tmp[i];
}
++n;
return ;
}
for(int i=0;i<8;++i) ///i代表1-8列
{
if(r==0){
tmp[r]=i; //如果为第0行,直接赋值,进入递归
queens(r+1);
}else{ //否则要先与之前的皇后进行比较,判断是否冲突
/* 错误点在这个else里面,主要是下面的if判断
我本想着当前遍历的这个点既不与之前放置的皇后同列也不与其在同一个
对角线上,那么这个点就可以放置一个皇后,于是按照这个逻辑写出了下
面这段代码 */
for(int j=0;j<r;j++) //r代表当前要放置皇后的行
{
if(i!=tmp[j] && abs(r-j)!=abs(i-tmp[j])){
tmp[r]=i;
queens(r+1);
}
}
/*上面这一段代码的实际逻辑是
循环遍历之前放置的皇后,并与当前的点进行比较,判断是否能放置皇后
一旦判断可以放置,立即放置皇后,并继续递归。
也就是说,只要与其中一个皇后不冲突,就可以放置,而不管是否与后面
的皇后是否冲突。这是不符合实际逻辑的。
正确的逻辑是:遍历之前所有皇后,只要有一个皇后与当前放置的点冲突
那么该点就不能放置皇后。
*/
}
}
}
int main(){
queens(0);
//输出所有可以放置皇后的点
for(int i=0;i<n;++i)
{
for(int j=0;j<8;++j)
{
cout<<all[i][j]<<" ";
}
cout<<endl;
}
cout<<n<<endl;
return 0;
}
那么我是如何知道原来的代码逻辑是错误的呢?非常简单粗暴的一个方法,输出中间变量值,通过观察这些中间变量值的变化过程来判断最终是哪里出现了问题。
代码只粘贴一部分
void queens(int r){ //r代表第r行
if(r==8){
for(int i=0;i<8;i++)
{
all[n][i]=tmp[i];
}
++n;
//首先是在每次得到最终结果的时候输出一下这个互不冲突的皇后序列
for(int i=0;i<8;i++)
{
cout<<tmp[i]<<" ";
}
cout<<endl;
return ;
}
for(int i=0;i<8;++i) ///i代表1-8列
{
if(r==0){
tmp[r]=i; //如果为第i行,直接赋值,进入递归
queens(r+1);
}else{ //否则要先与之前的皇后进行比较,判断是否冲突
for(int j=0;j<r;j++)
{
if(i!=tmp[j] && abs(r-j)!=abs(i-tmp[j])){
tmp[r]=i;
queens(r+1);
}
}
}
}
}
发现结果如下:
输出结果明显不对,但是为什么会得到这样的结果呢?为了进一步分析原因,继续输出中间变量。
void queens(int r){ //r代表第r行
if(r==8){
for(int i=0;i<8;i++)
{
all[n][i]=tmp[i];
}
++n;
for(int i=0;i<8;i++)
{
cout<<tmp[i]<<" ";
}
cout<<endl;
exit(0); //在这里退出主要是我们分析一组案例即可,其余同第一组
return ;
}
for(int i=0;i<8;++i) ///i代表1-8列
{
if(r==0){
//首先判断r=0时能否正常输出
cout<<"r==0"<<endl;
cout<<"tmp["<<r<<"]="<<tmp[r]<<endl;
tmp[r]=i; //如果为第i行,直接赋值,进入递归
queens(r+1);
}else{ //否则要先与之前的皇后进行比较,判断是否冲突
for(int j=0;j<r;j++)
{
if(i!=tmp[j] && abs(r-j)!=abs(i-tmp[j])){
//然后详细分析每一次tmp数组中的变化
cout<<"i="<<i<<" tmp["<<j<<"]="<<tmp[j]<<" abs(r-j)"<<abs(r-j)<<" abs(i-tmp[j])="<<abs(i-tmp[j])<<endl;
cout<<"tmp["<<r<<"]="<<i<<endl;
tmp[r]=i;
queens(r+1);
}
}
}
}
}
输出结果如下:
转换成文字是这样的:
/*
r==0
tmp[0]=0
i=2 tmp[0]=0 abs(r-j)1 abs(i-tmp[j])=2
tmp[1]=2
i=0 tmp[1]=2 abs(r-j)1 abs(i-tmp[j])=2
tmp[2]=0
i=1 tmp[0]=0 abs(r-j)3 abs(i-tmp[j])=1
tmp[3]=1
i=0 tmp[1]=2 abs(r-j)3 abs(i-tmp[j])=2
tmp[4]=0
i=0 tmp[1]=2 abs(r-j)4 abs(i-tmp[j])=2
tmp[5]=0
i=0 tmp[1]=2 abs(r-j)5 abs(i-tmp[j])=2
tmp[6]=0
i=0 tmp[1]=2 abs(r-j)6 abs(i-tmp[j])=2
tmp[7]=0
0 2 0 1 0 0 0 0
*/
首先第一组,r=0时,tmp[0]=0,结果没问题。
然后分析第二组,r=1时,tmp[1]=2,看起来也没问题,与第一个不冲突
然后第三组就有问题了,tmp[2]=0,明显与第一组冲突!!为什么会出现在结果集中,我们按照代码逻辑带入,发现,虽然与第一组冲突,但随着for循环进行,当再与第二组比较时发现不冲突了,于是,将它放到结果集中。此时,我们已经发现代码的逻辑问题了。
这里输出,i, tmp[i],abs(r-j),abs(i-tmp[j])主要目的是观察是否是if语句中的判断条件出现错误,经比较发现实际上并没有出现错误。
那么知道了正确的逻辑后,修正的代码如下(正确代码):
#include<iostream>
#include<cmath>
using namespace std;
int all[1000][10]={0};
int tmp[8]={0};
int n=0;
void queens(int r){ //r代表第r行
if(r==8){
for(int i=0;i<8;i++)
{
all[n][i]=tmp[i];
}
++n;
return ;
}
for(int i=0;i<8;++i) ///i代表1-8列
{
if(r==0){
tmp[r]=i; //如果为第i行,直接赋值,进入递归
queens(r+1);
}else{ //否则要先与之前的皇后进行比较,判断是否冲突
/*通过增加一个bool类型变量,来判定与之前所有皇后比较后是否能放置
该皇后,若不能放置,则flag为false。falg默认为true,即可以放置。
*/
bool flag = true;
for(int j=0;j<r;j++)
{
if(i==tmp[j] || abs(r-j)==abs(i-tmp[j])){
flag = false;
break;
}
}
if(flag){
tmp[r]=i;
queens(r+1);
}
}
}
}
int main(){
queens(0);
for(int i=0;i<n;++i)
{
for(int j=0;j<8;++j)
{
cout<<all[i][j]<<" ";
}
cout<<endl;
}
cout<<n<<endl;
return 0;
}
8皇后问题改进,这里我们发现每次判断能否在一个位置放置皇后,都要与之前所有皇后进行比较,且比较次数较多,那么有没有什么办法降低比较次数呢?
方法当然有。首先分析为什么要与之前的皇后进行比较,因为要判断当前节点是否与之前皇后是否在同一列或者同一对角线上。这时候我们可以思考,我们是否可以记录每个对角线的状态呢?这样直接读取对角线的状态而不需要一个个去比较的出了。基于这样的想法,我们可以写出如下算法。
#include<iostream>
#include<cmath>
using namespace std;
int all[1000][10]={0};
int tmp[8]={0};
int n=0;
bool col[8]={0}; //col记录每一列列的状态
bool L[15]={0}; //L记录左对角线的状态,一共15条左对角线
bool R[15]={0}; //R记录右对角线的状态,一共15条右对角线
void func(int r){
if(r==8){
for(int i=0;i<8;i++)
{
all[n][i]=tmp[i];
}
++n;
return ;
}
for(int c=0;c<8;++c)
{
int ld = (c-r)+7;
int rd = c+r;
//如果当前点的列和左右对角线都没被占据
if((!col[c]) && (!L[ld]) && (!R[rd]))
{ //标记当前点的列和左右对角线为占据状态
col[c]=1,L[ld]=1,R[rd]=1;
tmp[r]=c;
func(r+1);
//递归结束后,恢复未被占据的状态
col[c]=0,L[ld]=0,R[rd]=0;
}
}
}
int main(){
func(0);
for(int i=0;i<n;++i)
{
for(int j=0;j<8;++j)
{
cout<<all[i][j]<<" ";
}
cout<<endl;
}
cout<<n<<endl;
return 0;
}