您的位置:首页 > 理论基础 > 数据结构算法

N皇后问题的非递归回溯和递归回溯

2017-02-26 16:14 519 查看

一、问题描述

           一个n*n的棋盘,要在上面放n个皇后。规则:两个皇后之间如果是同列、同行、同对角线它们会互相攻击。也就是说:棋盘上的任意两个皇后皇后

不能为同列、同行、同对角线。


二、问题分析

          回溯法是一种通用的搜索算法,几乎可以用于求解任何可计算的问题。算法的执行过程就像是在迷宫中搜索一条通往出口的路线,总是沿着某一方向向前试探,若能走通,则继续向前进;如果走不通,则要做上标记,换一个方向再继续试探,直到得出问题的解,或者所有的可能都试探过为止。
首先需要对棋盘进行描述。直观地,棋盘可以用二维数组表示,有皇后的棋格对应数组元素值为1,无皇后的棋格对应数组元素值为0。但这种存储结构并不是最简单有效的选择。 
  下图中左边部分给棋盘的行、列编了号,提供的摆放方法,就是问题的一个解。右边的部分,将各行上皇后所在的列数记录下来,用这8个数字(4, 6, 8, 2, 7, 1, 3, 5),也构成了对问题解的一种描述。 


  

        由此可以看出,可以定义一个一维数组int x
;,用x[i]的值表示第i行上皇后所在的列数,n皇后问题的解可以用(x[1],
x[2], ….. x
)的形式描述。 
  解决了数据表示的问题,设计数据处理的方法。这里要用回溯的策略,设计计算机对n皇后问题的求解方法。以4皇后为例,如下图所示,在图8.(a)中,第1行第1列上放置一个皇后,图(b)中确定第2行的可能放法,在尝试第1列、第2列由于相互攻击而放弃之后,确定在第3列放置可以继续,在图(c)中继续对第3行进行考察,发现将所有4列都尝试过了,也没有办法将皇后安排一个合适的位置,对第4行做任何的尝试都没有意义,这时产生回溯,结果是在图(d)中将第2行的皇后安排到第4列,然后第3行的暂时可以放在第2列,在图(e)中试着确定第4行的皇后,却发现无解再次回溯,只能够如图(f)所示将第1行的皇后放到第2列,再经图(g)、(f)之后找到4皇后问题的一个解,那就是图(g)的(2,
4, 1, 3)。 


 
图8.22 用回溯找出4皇后问题一个解的过程

在图8.23中,给出了求出4皇后问题所有解的完整过程的描述。图中(1
* * *)对应图8.22(a)中第1行皇后安排在第1列,其他行待定的状态,接下来的(1 3 * *)对应了图8.22(b)中第2行皇后安排在第3列的状态。可以判断出在这个状态下,继续尝试并不能够完成求解,于是发生回溯(其下方的B代表回溯),于是下一个尝试的状态将是(1 4 * *),……。将这样的过程继续下去,能够找出4皇后问题的所有解(2 4 1 3)和(3 1 4 2),如图8.23中两个加网格背景的结点。 


 
图8.23 求出4皇后问题所有解的完整过程

搞清楚用回溯法求解的过程后,将关注如何基于(x[1],
x[2], ….. x
)形式的解结构,写出让计算机完成求解过程的代码。4皇后问题尚且可以在纸上画出解,8皇后问题的可能解有8!=40320种,最终解有92种,必须要依靠计算机求解了。 
  什么样的解才是可行的?需要描述出任何两个皇后可以“互相攻击”这样的条件: 
  (1)有两个皇后处在同一行:解的结构(x[1], x[2], ….. x
)已经保证同一行不会出现两个皇后。 
  (2)有两个皇后处在同一列:表示为x[i]=x[k],假如在图8.23中出现表示为(1 1 * *)、(4 2 3 2)之类的结点,则说明有两个皇后在同一列了。 
  (3)有两个皇后处在同一斜线:若两个皇后的摆放位置分别是第i行第x[i]列、第k行第x[k]列,若他们在棋盘上斜率为-1的斜线上,满足条件i-x[i]=k-x[k],例如(1 4 3 *)、(4 1 2 *);若他们在棋盘上斜率为1的斜线上,满足条件i+x[i]=k+x[k]。将这两个式子分别变换成i-k=x[i]-x[k]和i-k=x[k]-x[i],例如(3
4 1 *)。综合两种情况,两个皇后位于同一斜线上表示为|i-k|=|x[i]-x[k]|。 
  在下面的程序实现中,place(x, k)函数用于判断在第k行第x[k]列放置皇后,是否会与前面摆放好的皇后产生相互攻击。只要有某行(第i行)的皇后与这个第k行的皇后处在同一列(x[i]=x[k])或者处在同一斜线(|i-k|=|x[i]-x[k]|),则立即返回假(0),表示不可能构成解。 
  再接下来,就是在实现问题求解的nQueens(x, n)函数中,从第1行开始,逐行逐列地考察皇后的摆放,当遇到某一行所有可能情况试过不必再深入到下一行考察时,及时回溯到上一行,接着考察。 
  程序实现中,将保存解的数组定义成了动态数组。多分配一个单元,因为数组的首元素x[0]一直空闲未用,有用的单元是x[1]到x


根据以上分析,这个问题可以采用两种方法解决,一种是非递归回溯,另一种是递归回溯。详细请看以下代码:
/*
改程序采用了两种方案解决N皇后问题;
一种是采用非递归的回溯算法,第二种
是采用递归的回溯算法。
*/

#include<iostream>
#include<cmath>
#include<cstdio>
#include<cstdlib>

using namespace std;

//声明各个函数

void nqueen1(int *a, int n);  //非递归的方法
void nqueen2(int *a, int row, int n);  //递归的方法
bool valid(int *a, int k);    //判断在第k行,a[k]的皇后是否有效
void printqueen(int *a, int n); //打印输出皇后位置

int main()
{
int n;
cout << "请输入皇后的个数n:" << endl;
cin >> n;
int *a; //存放结果的数组的首地址
a = (int*)malloc(sizeof(int)*(n + 1));  //动态分配数组空间,a[0]空闲;

cout << "非递归回溯输出解决方案:" << endl;
nqueen1(a, n);

cout << "递归回溯输出解决方案:" << endl;
nqueen2(a, 1, n);
}

/*如果一个皇后能放在第k行第a[k]列,则返回真(1),否则返回假(0)*/
bool valid(int *a, int k)
{
int i;
for (i = 1; i < k; ++i)
{
/*如果前k-1行中有某行的皇后与第k行的在同一列或同一斜线,返回0*/
if ((a[i] == a[k]) || (abs(i - k) ==abs(a[i] - a[k])))
return false;
}
return true;
}

void printqueen(int *a, int n)
{
int i, j;
for (i = 1; i <= n; ++i)
{
for (j = 1; j <= n; ++j)
{
if (a[i] == j)
cout << "Q";
else
cout << "*";
}
cout << endl;
}
cout << endl;
}

void nqueen1(int *a, int n)
{
int count = 0;
int k;
k = 1;  //k
a[k] = 0;/*a[k]是当前列,进到循环中,立刻就会执行a[k]++,而选择了第1列*/
while (k > 0)
{
a[k]++; /*移到下一列*/
while (a[k] <= n && !valid(a,k)) /*逐列考察,找出能摆放皇后的列a[k]*/
a[k]++;
if (a[k] <= n)/*找到一个位置可以摆放皇后*/
{
if (k== n)/*是一个完整的解,输出解*/
{
count++;
cout << "这是非递归第" << count << "个解法。" << endl;
printqueen(a, n);  //此时k==n,再进入循环,考察第k行的下一列
}
else/*没有完成最后一行的选择,是部分解,转向下一行*/
{
k++; /*接着考察下一行*/
a[k] = 0;/*到循环开始执行a[k]++后,下一行将从第1列开始考察*/
}

}
else
{ /*对应a[k]>n的情形,这一行已经没有再试的必要,回溯到上一行*/
k--;
}/*上一行在原第a[k]列的下1列开始考察*/
}
}

int countbacak=0;
void nqueen2(int *a,int row, int n)
{
if (row > n) //此时调用开始返回
{
countbacak++;
cout << "这是递归第" << countbacak << "个解法。" << endl;
printqueen(a, n);
}
else
{
for (int i = 1; i <= n; ++i)  //i表示列,每个i,都对应一系列的递归调用
{
a[row] = i; //形成坐标
if(valid(a,row))
nqueen2(a,row+1, n);
}
}
}


参考:http://blog.csdn.net/sxhelijian/article/details/48914323
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息