八皇后详解

来源:互联网 发布:网络投稿平台 编辑:程序博客网 时间:2024/04/23 21:47

 

 

    还是先简单介绍下八皇后问题:

    八皇后问题是一个古老而著名的问题,是回溯算法的典型例题。该问题是十九世纪著名的数学家高斯1850年提出:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。

 

    解这题需要用到回溯法,先简单介绍递归和回溯。回溯与递归很像。它们的本质区别在于递归每次都能成功,沿着递归函数进去肯定能进入极值。举个求和的简单例子:

    //求1至n的和

  

 int sum(int n){if(1==n)    return 1;else    return n+sum(n-1); }


    所有数的和等于n加上前(n-1)个数的和,而前(n-1)个数又等于n-1加上前(n-2)个数的和,如此下去,前2个数的和等于2加上前1个数之和,前1个数的和就是1,程序运行至此就一直返回。

    在这个递归的例子中,每进入一个新函数,返回值最终都能成功。也就是说沿着函数一直进去肯定能找到n==1的情况。

 

    虽然回溯要想解决问题还得借助递归思想,但是回溯进入新的函数却不一定能找到答案,比如要从这张二叉树图片

中的根节点(0号)开始找到一个存放在11号叶结点的东西(之前你只知道东西放在某个叶结点而已),在没有任何帮助条件下,为了找到目标,你得挨个找,假设从左往右找。0号->1号->3号->7号,进到7号后你发现没有,这时你必须得返回至3号,然后往右移,进入8号,也没有,返回至3号,然后发现7号和8号找过了,于是从3号返回至1号,接着1号->4号->9号……就这么进去找,找不到就返回前一个再向右移,最后会进入到11号找到东西。

 

    这就是回溯法的思想,找不到就返回前一个,接着找,直到找到为止。

 

    八皇后的思路大致是这样的,挨行或者挨列放置皇后(这里先使用挨列),使其都不冲突。

    先在第一列放置皇后,记录放置的行号(行号从小到大挨个试,下同),再放置第二列(前提不与前面的皇后冲突),同样也记录行号,然后放置第三、第四列(前提也是不与前面的皇后冲突),这里注意一下,正在尝试第N列皇后的放置时说明前(N-1)列皇后放置是不冲突的。当发现某列无法放置皇后时,就返回前一列,行号加一(就跟上面二叉树例子一样,返回一步后,向右一个试探,这里按行号从小到大试探),继续试探。如果放置好了第八列的皇后就得到了一种放置的方法。接着行号加一或者返回前一列,继续试探,直到把所有的情况都试完。

 

现在把代码贴上来:注释足够多,讲得应该很清楚了。

 //#include <iostream.h>#include <stdio.h> #define N 8 int site[N];//保存各列皇后放置的行数,下标表示列号(0-7),site[i]保存放置行号(1-8)int iCount=0;//表示可行解的数目 void Queen(int n);//执行并放置第n列皇后的回溯函数,参数n表示第n列(从0开始)bool isValid(int n);//判断第n列放置的皇后是否与前面的冲突,冲突返回false,有效返回truevoid outPut();//找到一个可行解后,把可行解输出来 int main(){//从第0列开始放置皇后Queen(0);//任意输入结束程序//getchar();return 0;}void Queen(int n){int i;//N==n时,表示前面的8列(列号为0-7)都已经放好,把一组解输出if (N==n){outPut();return;}//i表示行号,第n列皇后将放置的行号for (i=1;i<=N;i++){site[n]=i;//这条语句保证了每一列皇后的放置都会从第1行试探到第N行//如果放置第n列成功,则继续尝试第n+1列if (isValid(n))Queen(n+1);}}bool isValid(int n){int j;for (j=0;j<n;j++){if (site[n]==site[j]||(site[n]-site[j])==(n-j)||(site[n]-site[j])==(j-n))return false;//site[n]==site[j]表示放置的行号相同,冲突//因为是逐列操作,不存在放置的列号相同//site[n]-site[j]==n-j,斜率为1,表示在斜率为1的对角线上//site[n]-site[j]==j-n,斜率为-1,表示在斜率为-1的对角线上}return true;}void outPut(){int j;iCount++;printf("%-4d",iCount);for (j=0;j<N;j++){printf("%3d",site[j]);}printf("/n");}


上面的代码没有设置冲突状态,都是要判断时才临时算的,所以退回去时不需要更改原来的状态。相信大家也看过下面保存冲突与否的状态的代码。
//这代码是逐行放置皇后的,

#include <stdio.h>bool col[8]={false};//竖直线(列线),用于记录列线冲突状态bool Left[15]={false};//左对角线,用于记录左对角线冲突状态,拿笔画画就知道有15条左对角线了bool Right[15]={false};//右对角线,用于记录右对角线冲突,同样也有15条右对角线int site[8]={0};//保存每行放置皇后的列号int sum=0;//记录可行解数int row=0;//行号,本代码是逐行放置的,这里的row是可以当做Queen函数的参数的。void Queen();//如果row作为Queen的参数,这条语句变成void Queen(int row);int main(){Queen();//如果row作为Queen的参数,这条语句变成Queen(0);return 0;}//这里给出Queen函数的两种写法,趋向于第二种写法,因为把极值单独列出来很直观,思路容易跟上void Queen(){//如果row作为Queen的参数,这条语句变成void Queen(int row){/*函数写法一//对于每行,都从第0列试到第7列,l表示列号for (int l=0;l<8;l++){//col[l]判断列号是否相同我想应该很容易就理解,判断对角线的两个数组可能费劲点//Left[row+l],row表示行号,l表示列号,这样理解,从左上角开始,下移到行号的地方,然后//再右移到列号的地方,那个点所在的对角线就记录在Left[row+l]里//Right[row-l+7],理解成Right[row+7-l],从左上角开始,下移至行号的地方,然后移到最右,//接着左移列号个单位,那个点所在的对角线就记录在Right[row+7-l]了if (!col[l]&&!Left[row+l]&&!Right[row-l+7]){//判断这个点可以放置后,就记录列号,并修改状态site[row]=l;col[l]=true;Left[row+l]=true;Right[row-l+7]=true;//然后行号加1;row++;//如果row作为Queen的参数,这条语句不要//row==8时,说明前8行都放好了(行号从0开始)if (row==8){//可行解加一sum++;printf("%-4d",sum);for (int i=0;i<8;i++)printf("%4d",site[i]);printf("/n");}else//如果row作为Queen的参数,这条语句变成Queen(row+1);Queen();//row<8时,接着放置下一行的皇后,//如果失败,则返回到前一行,返回后要立马恢复原来的状态。row--;//如果row作为Queen的参数,这条语句不要col[l]=false;Left[row+l]=false;Right[row-l+7]=false;}}*///函数写法一//函数写法二if (row==8){sum++;printf("%-4d",sum);for (int i=0;i<8;i++)printf("%4d",site[i]+1);printf("/n");return;}int l;for (l=0;l<8;l++){//site[row]=l写在这里也可以if (!col[l]&&!Left[row+l]&&!Right[row-l+7]){site[row]=l;col[l]=true;Left[row+l]=true;Right[row-l+7]=true;row++;Queen();//一返回上一级就得立马恢复原来的状态row--;col[l]=false;Left[row+l]=false;Right[row-l+7]=false;}}//写法二}


好了,到这里相信大家对八皇后都很熟悉了。对回溯也有更深的了解了,不过还得多做些题,递归和回溯好用但不好理解。
稍微改改就可以得到任意皇后的排列代码了。
结果是:
皇后数 解个数
1 1
2 0
3 0
4 2
5 10
6 4
7 40
8 92
9 352
10 724
11 2680
12 14200
.
.

 

 

原创粉丝点击