1.递归与回溯
1.1递归
- 乍一听很高深,其实理解起来很轻松,但是面对问题时如何动手编写递归程序却十分棘手!
- 递归程序的流程图很清晰,非常直观。所谓递归,简单点来说,就是一个函数直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。
- 递归与循环有本质区别:简单来说,循环是有去无回,而递归则是有去有回(因为存在终止条件)。
- 递归与栈的关系:常常听到 “递归的过程就是出入栈的过程”,这句话怎么理解?我们以阶乘代码为例,取 n=3,则过程如下:
-
第 1~4 步,都是入栈过程,Factorial(3)调用了Factorial(2),Factorial(2)又接着调用Factorial(1),直到Factorial(0);
-
第 5 步,因 0 是递归结束条件,故不再入栈,此时栈高度为 4,即为我们平时所说的递归深度;
-
第 6~9 步,Factorial(0)做完,出栈,而Factorial(0)做完意味着Factorial(1)也做完,同样进行出栈,重复下去,直到所有的都出栈完毕,递归结束。
-
每一个递归程序都可以把它改写为非递归版本。我们只需利用栈,通过入栈和出栈两个操作就可以模拟递归的过程,二叉树的遍历无疑是这方面的代表。但是并不是每个递归程序都是那么容易被改写为非递归的。某些递归程序比较复杂,其入栈和出栈非常繁琐,给编码带来了很大难度,而且易读性极差,所以条件允许的情况下,推荐使用递归。
1.2回溯
- 回溯法,又被称为“试探法”。解决问题时,每进行一步,都是抱着试试看的态度,如果发现当前选择并不是最好的,或者这么走下去肯定达不到目标,立刻做回退操作重新选择。这种走不通就回退再走的方法就是回溯法。
- 回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。
- 回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。
- 回溯法指导思想——走不通,就掉头。设计过程:确定问题的解空间;确定结点的扩展规则;搜索。
2.八皇后问题
2.1何为八皇后问题?
在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问一共有多少种摆法。
2.2八皇后问题分析
-
一个皇后的封锁范围
-
两个皇后的封锁范围
-
如此继续下去,能安放下一位皇后的位置越来越少,那么我们最终如何能安放完这8位皇后呢?
-
首先我们看一下特别暴力的方法:从8x8的格子里选8个格子,放皇后,然后测试是否满足条件,若满足则结果加1,否则换8个格子继续试。很显然,64选8,并不是个小数字,十亿级别的次数,够暴力。如果换成围棋的棋盘,画面就会太美而不敢算。
-
稍加分析,我们可以得到另一个不那么暴力的方法:显然,每行每列最多只能有一位皇后,如果基于这个事实再进行暴力破解,那结果会好很多。安排皇后时,第一行有8种选法,一旦第一行选定,假设选为(1,i),那么第二行只能选(2,j),其中,j不等于i,所以有7种选法。以此类推,需要穷举的情况有8!=40320种,比十亿级别的小很多了。
-
这看起来已经不错了,但尝试的次数还是随着问题规模按阶乘水平提高的,我们仍然不满意,所以,“递归回溯”的思想就被提出了,专治这种问题。
2.3八皇后问题算法设计
-
变量初始化
place: int数组(0…7),第n行皇后所占位置的列号
flag:bool数组(0…7),表示col列是否可放皇后
d1:bool数组(0…14),(n,col)所在上对角线是否可以放皇后
d2:bool数组(0…14),(n,col)所在下对角线是否可以放皇后 -
核心函数:实现递归与回溯
void gernerate(int n){ //main函数开启函数:n=0,从第0行寻找合适的位置
int col; //col代表棋盘各列
for(col=0;col<8;col++){ //第n行逐列遍历
if(flag[col]&&d1[n-col+7]&&d2[n+col]){ //判断条件就是列、上下对角线是否占领
place[n]=col; //一旦满足判断条件就要将该位置记录进place数组
flag[col]=0; //当然也要占领该列即col列
d1[n-col+7]=0; //占领该上对角线
d2[n+col]=0; //占领该下对角线
if(n<7){ //如果n<7说明没到棋盘最后一行,于是进入递归搜寻下一行的合适位置
gernerate(n+1);
}
else print(); //递归终止条件,打印place数组,这个函数不是难点
//如果递归函数只有以上部分是无法求出所有解的
//回溯,取消该列,该上下对角线的占领,
flag[col]=1;
d1[n-col+7]=1;
d2[n+col]=1;
//进行完上面三行回溯代码后,继续遍历下面的列直至第n行所有列遍历结束,此时返回上一层递归函数,先回溯然后继续向该行其他列遍历
}
}
}
代码与结果展示
结果共92种情况:1代表皇后的位置。
代码如下,使用c语言编写,可以转换成C++
#include <stdio.h>
int place[8]= {0};//第N个皇后所占位置的列号
bool flag[8]= {1,1,1,1,1,1,1,1}; //标志数组,第col列是否可占,1标识true可占
bool d1[15];//表示上对角线是否可占
bool d2[15];//表示下对角线是否可占
int times=1;
void print();
void gernerate(int n);
int main(){
for(int j=0;j<15;j++){
d1[j]=1;
d2[j]=1;
}
gernerate(0);
}
void gernerate(int n){
int col;
for(col=0;col<8;col++){ //第n行逐列遍历
if(flag[col]&&d1[n-col+7]&&d2[n+col]){
place[n]=col;
flag[col]=0;
d1[n-col+7]=0;
d2[n+col]=0;
if(n<7){
gernerate(n+1);
}
else print();
//回溯,取消占领
flag[col]=1;
d1[n-col+7]=1;
d2[n+col]=1;
}
}
}
void print(){
printf("第%d种情况:\n",times);
for(int m=0;m<8;m++){
for(int n=0;n<8;n++){
if(n==place[m]){
printf("1 ");
}
else printf("0 ");
}
printf("\n");
}
printf("# # # # # # # #\n");
times++;
}