本节知识点:
1.递归与回溯:
a.回溯算法的基本思想:从问题的某一种状态出发,搜索可以到达的所有状态。当某个状态到达后,可向前回退,并继续搜索其他可达状态。当所有状态都到达后,回溯算法结束!
b.
对于回溯算法,在前面KMP匹配中就利用了这个思想,只不过当时KMP中定义了一个node数组(起到了一个地图的作用,记录了每种回溯情况的可能)。而这节中,是利用函数的活动对象保存回溯算法的状态数据,因此可以利用递归完成回溯算法!
2.八皇后问题:
a.问题背景:国际象棋是一个8*8的矩阵,在棋盘中同时放下8个皇后,且互相不攻击的情况叫八皇后问题
b.如图,说明八皇后问题中的回溯算法:
注意:其实就是不断的通过递归函数,去往棋盘中尝试放皇后,成功就继续递归(即继续放皇后),失败就跳出递归函数,回溯到上层递归函数中,上层递归函数中保存着上一个皇后的位置!!!这就是八皇后中,回溯的概念!
c.
八皇后的算法思路:
第一、为了更加方便我们表示棋盘,我们使用一个10*10的二维数组来表示8*8的棋盘加棋盘边框。
char board[10][10];
第二、初始化棋盘,把棋盘边框标记为'#',把棋盘中的位置都标记为‘ 空格 ’
void init()
{
int i = 0;
int j = 0;
for(i=0; i<N+2; i++)
{
board[0][i] = '#';
board[N+1][i] = '#';
board[i][0] = '#';
board[i][N+1] = '#';
}
for(i=1; i<=N; i++)
{
for(j=1; j<=N; j++)
{
board[i][j] = ' ';
}
}
}
第三、定义一个棋盘的打印函数display(),方便测试
void display()
{
int i = 0;
int j = 1;
for(i=0; i<N+2; i++)
{
for(j=0; j<N+2; j++)
{
printf("%c", board[i][j]);
}
printf("\n");
}
}
第四、
我们定义一个检查的函数check(),来判断这个位置是否可以放皇后,对于检查,我们检查三个方向,左上(-1,-1) 、 右上(-1,1) 、正上(-1,0) 对于横排我们是不检测的,因为我们每一排就放一个皇后,皇后用' * '来表示!
typedef struct _tag_pos //定义一个数据结构来充当方向
{
int i;
int j;
}Pos;
/*检测三个方向 左上 右上 正上 横排是不检测的 因为一排只放一个*/
static Pos pos[3] = {{-1,-1},{-1,1},{-1,0}};
int check(int i, int j)
{
int p = 0;
int ret = 1;
for(p = 0; p < 3; p++) //检测三个方向
{
int ni = i;
int nj = j;
while(ret && (board[ni][nj] != '#'))//判断没有到达棋盘边界
{
ni = ni + pos[p].i;
nj = nj + pos[p].j;
ret = ret && (board[ni][nj] != '*');//判断这个方向没有放过皇后
}
}
return ret; //可以放皇后返回1 不可返回0
}
第五、
是八皇后算法的核心,就是放皇后的函数find(),这个函数是这样的!先从第一行开始,进行对列数j的for循环,在for循环中判断(i,j)是否可以放皇后,一旦可以放皇后,就放置一个皇后,然后find(i+1),进入第二行(即第二层递归)。继续做同样的判断,这个递归函数有两个出口,同理,就有两种情况结束递归,一种是,i超过了第八行,也就是说,皇后全部放置完全,所以应该display()打印!另一种是,for循环结束,依然没有check()成功,说明前面有皇后放错了!导致这一行不能放皇后!应该进行回溯!!!所以结束这个错误的递归,返回到上一层递归,清除上层递归中放错的皇后 board[i][j] = ' '; 继续进行上一行的for循环!!!这里需要补充的是,即使i超过了第八行,皇后放置完全了,函数依然也会返回到上一行,去进行上一行的for循环,擦除皇后位置,尝试寻找新八皇后的情况!!!
void find(int i)
{
int j = 0;
if(i > N) //判断是否已经超过了第八行
{
coust++; //计算八皇后情况的个数
display();
//getchar();
}
else
{
for(j = 1; j <= N; j++) //判断一行 是否有匹配的位置
{
if(check(i,j))
{
board[i][j] = '*'; //放置皇后
find(i+1);
board[i][j] = ' '; //清除放错的皇后
}
}
}
}
注意:之所以说,八皇后中利用递归来进行回溯,就是因为当递归结束的时候,会返回到上一层放置皇后的位置(即栈空间中,保存了上行皇后i,j的情况)
看着上面的动态想象下,当有时候,出现过的皇后又消失了,就是因为for循环结束了,还没有check成功,导致递归结束,返回到上层递归,并且擦除上层皇后的位置,并且继续for循环,如果这层for中check依然不成功,继续回溯,这个消失的过程就是回溯过程!与动图不同的是,我们的程序,不会在找完一种情况后就结束!即使成功匹配,程序也会向上层回溯,去寻找其它的情况,直到第一行for循环结束(即 i=1 的时候),程序才会停止!
本节代码:
八皇后问题的完整代码:#include <stdio.h>
#define N 8
static char board[N+2][N+2];
static int coust = 0; //记录八皇后个数
typedef struct _tag_pos //定义一个数据结构来充当方向
{
int i;
int j;
}Pos;
/*检测三个方向 左上 右上 正上 横排是不检测的 因为一排只放一个*/
static Pos pos[3] = {{-1,-1},{-1,1},{-1,0}};
void init()
{
int i = 0;
int j = 0;
for(i=0; i<N+2; i++)
{
board[0][i] = '#';
board[N+1][i] = '#';
board[i][0] = '#';
board[i][N+1] = '#';
}
for(i=1; i<=N; i++)
{
for(j=1; j<=N; j++)
{
board[i][j] = ' ';
}
}
}
void display()
{
int i = 0;
int j = 1;
for(i=0; i<N+2; i++)
{
for(j=0; j<N+2; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
int check(int i, int j)
{
int p = 0;
int ret = 1;
for(p = 0; p < 3; p++) //检测三个方向
{
int ni = i;
int nj = j;
while(ret && (board[ni][nj] != '#'))//判断没有到达棋盘边界
{
ni = ni + pos[p].i;
nj = nj + pos[p].j;
ret = ret && (board[ni][nj] != '*');//判断这个方向没有放过皇后
}
}
return ret; //可以放皇后返回1 不可返回0
}
void find(int i)
{
int j = 0;
if(i > N) //判断是否已经超过了第八行
{
coust++; //计算八皇后情况的个数
display();
//getchar(); //起到断点的作用
}
else
{
for(j = 1; j <= N; j++) //判断一行 是否有匹配的位置
{
if(check(i,j))
{
board[i][j] = '*'; //放置皇后
find(i+1);
board[i][j] = ' '; //清除放错的皇后
}
}
}
}
int main()
{
init();
//display();
find(1);
printf("coust is %d \n",coust);
return 0;
}
注意:八皇后问题,一共有92种情况!
课后思考:
本节遗留了两个问题没有解决:
1.对于全排列中有重复字母的情况,本节代码是解决不了的,会出现重复的情况,但是我觉得解决这个问题,就不应该使用递归思想去处理全排列了!!!
2.对于回溯算法,可以在迷宫寻找出口的程序中得到很好的应用,值得思考下!