本节主要讲八皇后问题的基本规则和递归回溯算法的实现以及具体的代码实现和代码分析。
转载请注明出处。http://write.blog.csdn.net/postedit/10813257
一、八皇后问题和递归回溯算法
1.八皇后是一个递归回溯算法的典型问题,问题的由来是这样的,在国际象棋中有8*8个位置,那么我们有8个皇后,我们要把8个皇后分别放在不同的行,不同的列和不同的对角线上,也就是说我们要让这8个皇后不能相互攻击。
2.八皇后问题最好的解决办法是回溯算法,回溯算法的基本思路如下:
①从问题的某一状态出发,搜索可以到达所有状态②当某个状态到达后,可向前回退,并继续搜索其他可达状态③当所有状态都到达后,回溯算法结束
二、八皇后问题的基本解决思路
1.我们首先对棋盘进行规定,为了方便查看我们将棋盘定义边框。因为原来真正可以安置皇后的位置是8*8,再加上棋盘的边框,那么我们就编程了10*10的棋盘了,这样我们可以来定义一个二维数组数组来描述棋盘,这里我们将描述棋盘的二维数组定义如下:
board[10][10];
2.棋盘定义结束后,我们要打印出棋盘的边框,同时真正的8*8棋盘的位置是要空出来的,因为这个时候我们还没有进行回溯算法,所以我们根本不确定到底在哪个位置安放皇后。棋盘的初始化函数我们定义为init(),具体实现代码如下:
void init ()
{ int i = 0;
int j = 0;
for (i = 0; i < 10; i++)
{
board[0][i] = '#';
board[i][0] = '#';
board[i][9] = '#';
board[9][i + 1] = '#';
}
for (i = 1; i <= 8; i++)
{
for (j = 1; j <= 8; j++)
{
board[i][j] = ' ';
}
}
}
在棋盘的初始化代码中,我们注意点是:
board[0][i] = '#';
board[i][0] = '#';
board[i][9] = '#';
board[9][i] = '#';
这里我们实际上分别对二维数组的第0行的每一个位置置为‘#’,然后对二维数组的第0列的每一个元素置为‘#’,再对二维数组的第10列中的每一个元素置为‘#’,最后对二维数组的第十行中的每一个元素置为‘#’。
第二个for循环的作用就是把这个棋盘所在的二维数组中的其他元素都置为空,菜鸟就是菜鸟,我在手敲代码的时候居然忽略了数组的开闭空间问题,所以导致打印出错。
3.对棋盘初始化结束后,我们为了方便以后的测试,这里我们先写一下打印函数,打印函数用于打印数组的每一个元素,这里将打印函数定义为output。具体代码如下:
void output()
{
int i = 0;
int j = 0;
for (i = 0; i < 10; i++)
{
for (j = 0; j < 10; j++)
{
printf ("%c", board[i][j]);
}
printf ("\n");
}
}
在打印函数中需要注意的是每一打印完数组中的一行内容以后,我们都要打印一个换行,要不然我们是绝对看不到棋盘的形状的。
4.两个外围函数写完了以后也就真正的进入了主角函数,主角函数用来查找那些位置可以放置皇后。将主角函数命名为find(),具体的实现代码如下:
void find(int i)
{
int ret = 0;
int j = 1; if (i > 8)
{
output();
getchar();
}
else
{
for (j = 1; j <= 8; j++)
{
if (check (i, j))
{
board[i][j] = '*';
find(i + 1);
board[i][j] = ' ';
}
}
}
}
首先我们队传进来的i的值进行判定,当然i的值通常都为1,因为我们都是要从1开始进行遍历的,然后我们开始进行遍历,进入for循环,进行递归和回溯算法。这里的具体程序执行步骤我会在下面继续说,这里暂且不提。
5.在上面的find()函数中,我们会发现,我们调用了一个check()函数,这个函数的主要作用是用来判定我们当前元素的位置是否合法,然后把判定的结果返回给find,供find在if判定中使用。下面是check函数的源代码:
int check (int i, int j)
{
int ret = 1;
int k = 0; for (k = 0; k < 3; k++)
{
int ni = i;
int nj = j; while (ret && (board[ni][nj] != '#'))
{
ni = ni + pos[k].heng;
nj = nj + pos[k].zong; if (board[ni][nj] == '*')
{
ret = 0;
}
else
{
ret = 1;
}
}
}
return ret;
}
在check函数中,我们首先进入for循环,当然变量k的定义主要是用来作为循环变量了。返回值ret先定义为1,如果可以放置则返回1,如果不可以放置则返回0。我们每一次进入for循环都将传进来的i,j的值赋给ni,nj,这样在进入while中时候我们可以对位置坐标进行偏移操作,我们通过for循环和while大循环可以还有坐标的移位操作可以对当前元素所在的位置相对于纵向所在的线,对角线所在的线。这里通过传进来的元素位置来确定偏移的原理图如下:
假定我们向check函数传递的是i,j.这样我们需要遍历的方向是红色箭头所指的三个方向,分别有①,②,③,④,⑤,⑥三个位置需要遍历,查看是否有*全部都没有星号,那么我们最后向find函数返回1,如果有一个位置有星号,我们向find函数返回0.
因为我们定义的是二维数组board[8][8]的棋盘,所以可以知道①的坐标是(i-1,j-1),②的坐标是(i-2,j-2),③的坐标是(i-1,j),④的坐标是(i-2,j),⑤的坐标是(i-1,j+1),⑥的坐标是(i-1,j+2),因为在check函数中我们使用的是while循环来控制坐标偏移的,代码如下:
while (ret && (board[ni][nj] != '#'));
这样我们不遇见#这个while循环不会终止,所以我们可以使用一个二维数组来控制这个偏移量,为了代码的阅读,我们同时还将偏移量的具体变量定义放在了一个结构体中。代码如下:
typedef struct _tag_pos
{
int heng;
int zong;
}poss;
poss pos[] = {{-1, -1}, {-1, 0}, {-1, 1}} ;
通过上面的定义我们会发现我们执行while大循环,我们都会将现有的坐标位置来和坐标的偏移量进行相加,直至棋盘的边框位置,这样就完成了红色箭头的三个方向所有位置的遍历。
再来说一下,check函数的执行机制,因为主要是两个循环语句在控制,所以这里做了一个excel表格,截图传上来,方便观看。
6.完成了每一个位置的合法性判定以后,我们再来回头看find函数,find函数是真正的递归回溯算法的实现。
我们这里以程序的第一轮判定为例进行说明:
find函数调用check函数并依次执行for循环,以行为单位将皇后安排在合法的位置上。程序指定到第6次调用find函数的时候,通过检测发现已经在第六行已经没有合法的位置的可以安排皇后了,这样find(6)函数返回到函数开始执行的位置,顺序向下执行,执行语句board[i][j] = ' ';这条语句的作用是把第五行的所有位置清空,然后程序继续返回到find(5)函数中执行,重新遍历,然后再依次执行,这样完成了回溯算法的实现。
当每次执行完find(8)函数后程序都会返回到上一层函数,然后再进行依次执行,直到递归调用的结束。
来个动图嗨皮一下,顺便脑海里想着它的遍历的历程。
三、八皇后问题的代码实现
#include <stdio.h> typedef struct _tag_pos
{
int heng;
int zong;
}poss; /*定义偏移量的数组*/
poss pos[] = {{-1, -1}, {-1, 0}, {-1, 1}} ; /*用来描述棋盘的数组*/
static char board[10][10]; /***********************************************************************************************
*函数名: init
*参数:void
*返回值:void
*功能:创建棋盘 同时创建边界
***********************************************************************************************/
void init ()
{ int i = 0;
int j = 0;
for (i = 0; i < 10; i++)
{
board[0][i] = '#';
board[i][0] = '#';
board[i][9] = '#';
board[9][i] = '#';
}
for (i = 1; i <= 8; i++)
{
for (j = 1; j <= 8; j++)
{
board[i][j] = ' ';
}
}
} /***********************************************************************************************
*函数名:output
*参数:void
*返回值:void
*功能:输出棋盘中每一个元素
***********************************************************************************************/
void output()
{
int i = 0;
int j = 0;
for (i = 0; i < 10; i++)
{
for (j = 0; j < 10; j++)
{
printf ("%c", board[i][j]);
}
printf ("\n");
}
} /***********************************************************************************************
*函数名:check
*参数:void
*返回值:合法返回1,不合法返回0
*功能:传进来的是现在位置,然后根据偏移量,判定这个位置是否合法
***********************************************************************************************/
int check (int i, int j)
{
int ret = 1;
int k = 0; for (k = 0; k < 3; k++)
{
int ni = i;
int nj = j; while (ret && (board[ni][nj] != '#'))
{
ni = ni + pos[k].heng;
nj = nj + pos[k].zong; if (board[ni][nj] == '*')
{
ret = 0;
}
else
{
ret = 1;
}
}
}
return ret;
} /***********************************************************************************************
*函数名:find
*参数:void
*返回值:int i 从主函数传进来的参数,用于规定从哪一行开始
*功能:查找共有多少种方法
***********************************************************************************************/
void find(int i)
{
int ret = 0;
int j = 1; if (i > 8)
{
output();
getchar();
}
else
{
for (j = 1; j <= 8; j++)
{
if (check (i, j))
{
board[i][j] = '*';
find(i + 1);
/*
假如上面的find(i + 1)调用以后,执行到if (check (i, j) )
这个时候不满足条件,程序跳出,然后find(i + 1)返回,因为这个位置的条件不满足,
所以程序继续向下执行,在for循环里继续循环,直到递归到最里层。然后每一个函数在
返回的时候都会执行board[i][j] = ' '
*/
board[i][j] = ' ';
}
}
}
} int main()
{
init();
find(1);
return 0;
}