最近在看数据结构时,我发现了很多关于递归回溯的问题,特别是在学习了树和图等非线性结构后,对递归回溯的问题也有了一些更深的认识,但是有时候我们可能不是很能分清楚这二者。那么,接下来我们来一起看看这二者的关系如何呢?
递归,是一种算法结构,一个递归就是在函数中调用函数本身来解决问题。在描述问题的某一状态时,需要用到该状态的上一状态,而描述上一状态,又需要用到上一状态的上一状态……这种用自已来定义自己的方法,称为递归定义。当然递归也不是一直无休止的自身调用,他总需要有一个递归出口条件。比如最简单的递归算法,求阶乘,n的阶乘f(n) = n * f(n-1),这就是一个自身调用自身的例子,他的出口条件当然就是1! = 1和0! = 1。而回溯,是一种算法思想,他是通过不同的尝试来生成问题的解,以深度优先方式系统搜索问题的解,有点类似于穷举,但是和穷举不同的是,回溯会“剪枝”,也就是对已经知道错误的结果没必要再枚举接下来的答案了。他适用于解决组合数较大的问题。
更深层次的了解递归回溯算法对许多问题的求解都很有帮助,下面就跟我来看看很经典的几个递归回溯的问题的求解。
1. 八皇后问题
在8*8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。(棋盘的规格可以改变,以下代码以8*8为例)
/*八皇后问题之C语言的实现*/
/*下面是关于代码的一些说明:
*把棋盘看作一个坐标系,其中第一象限为放置皇后的位置。
*我们知道各个皇后不能够放置在同一行,所以简化问题,只需要记下每行的皇后的列标就行。
*然后用queen这个数组来记录每行皇后的位置,其中数组的下标i+1代表的是第i行的皇后
*count的值代表的是该问题的解法数量
*/
#include <stdio.h>
/*关于函数的声明*/
/*该函数用于求解在(pointi, pointj)的当前位置上能不能放置皇后。*/
bool search(int pointi, int pointj);
/*该函数用于放置八个皇后,queenNumber的值代表的是当前放置的是第几个皇后。*/
void queenLend(int queenNumber);
int queen[8] = {-1, -1, -1, -1, -1, -1, -1, -1};
int count = 0;
int main() {
printf("\t\t\t每行皇后放置的位置:\n");
printf("\t---------------------------------------------------\n");
queenLend(0); /*从下标为0开始放置皇后*/
printf("\t---------------------------------------------------\n");
printf("\t共有count = %d 种放置皇后的方法。\n", count);
return 0;
}
bool search(int pointi, int pointj) {
for(int i = 0; i < pointi; i++) {
if(pointj == queen[i]) return false; /*判断该列上是否已经有皇后*/
if(pointi - i == pointj - queen[i]) return false; /*判断该坐标所在的主对角线上是否已经有皇后*/
if((pointi - i)+ (pointj - queen[i]) == 0) return false; /*判断该坐标所在的副对角线上是否已经有皇后*/
}
return true;
}
void queenLend(int queenNumber) {
for(int i = 0; i < 8; i++) { /*遍历第queenNumber行的0-7列*/
if(search(queenNumber, i)) {
/*第queenNumber行第i列符合条件 */
queen[queenNumber] = i;
/*找到一组解,count++,回溯 */
if(queenNumber == 7) {
count++;
printf("\tNumber %-3d", count);
for(int i = 0; i < 8; i++) {
printf("%5d", queen[i] + 1);
}
printf("\n");
return;
}
/*还有皇后没放完,处理下一个皇后*/
int nextNumber = queenNumber + 1;
queenLend(nextNumber);
}
}
/*该行没有位置放皇后,回溯到上一个皇后*/
/*切记!!!queen[]数组在上行的操作要删除*/
queen[--queenNumber] = -1;
return;
}
附上代码运行的图片!
2. 马走棋盘
在8*8的方格棋盘上,从任意方格出发,为马找一条走遍每一格且只走一次的路径。(棋盘的规格可变,以下代码以8*8为例)
/*马走棋盘。在8*8的方格棋盘上,从任意方格出发,为马找一条走遍每一格且只走一次的路径。*/
/*马走棋盘问题之C语言的实现*/
/*下面是关于代码的一些说明:
*把棋盘看作一个坐标系,其中第一象限为马要走的位置。
*因为所求的路径比较长,所以默认马从(0,0)出发,本程序只给出了马从(0,0)出发的问题的求解
*count的值代表的是该问题的解法数量
*/
#include <stdio.h>
/*该结构体表示马走的路径,line表示横坐标,column表示纵坐标*/
struct horseSpace {
int line;
int column;
}space[64]; /*该数组表示马每步走的点的横纵坐标*/
int count = 0;
/*x和y数组都是存放从上一个位置走日字到下一个位置所需要的坐标变化*/
int x[8] = {-2, -2, -1, -1, 1, 1, 2, 2};
int y[8] = {-1, 1, -2, 2, -2, 2, -1, 1};
/*下面是关于函数的声明*/
/*该函数用来求解马行走的轨迹, track代表当前为第几步*/
void horseTrack(int track);
/*该函数用于求解马能不能在当前的位置上行走*/
bool isAvailable(int track, int pointi, int pointj);
int main() {
space[0].line = space[0].column = 0;
/*初始化坐标*/
for(int i = 1; i < 64; i++) {
space[i].column = space[i].line = -1;
}
horseTrack(1);
printf("马从(0, 0)位置开始走棋盘的走法共有count = %d 种\n", count);
return 0;
}
void horseTrack(int track) {
for(int k = 0; k < 8; k++) {
int xx = space[track - 1].line + x[k];
int yy = space[track - 1].column + y[k];
/*判断当前位置是否合法*/
if(isAvailable(track, xx, yy)) {
/*若该位置合法,记录下来这个位置*/
space[track].line = xx;
space[track].column = yy;
if(track == 63) {
count++;
int amount = 0;
printf("\ncount = %d\n", count);
/*输出马走完棋盘所依次经过的路径*/
for(int j = 0; j < 64; j++) {
amount++;
printf("<%d, %d> ", space[j].line, space[j].column);
if(amount % 8 == 0)
printf("\n");
}
return;
}
/*还有坐标没走完,继续下一步*/
else {
int nexttrack = track + 1;
horseTrack(nexttrack);
}
}
}
/*该位置没有路可以走,回溯到上一个位置*/
/*切记!!!space[]数组的上一个位置不合理,要删除上个位置的信息*/
track--;
space[track].line = -1;
space[track].column = -1;
return;
}
bool isAvailable(int track, int pointi, int pointj) {
/*判断位置是否合法*/
if(pointi < 0 || pointj < 0 || pointi >= 8 || pointj >= 8)
return false;
/*判断该位置马是否已经走过*/
for(int i = 0; i < track; i++) {
if(pointi == space[i].line && pointj == space[i].column) {
return false;
}
}
return true;
}
以下是代码的部分运行时的图片!
3. 素数环
把从1到20这20个数摆成一个环,要求相邻的两个数的和是一个素数。(这里的数的范围是可以改变的,以下以1-20为例)
/*八皇后问题之C语言的实现*/
/*下面是关于代码的一些说明:
*b数组是保存素数环顺序的变量
*因为求解的最后结果为一个环,所以默认有环头,且环头为 1
*count是记录排列总数的变量
*/
#include <stdio.h>
#include <math.h>
/*该数组用来记录素数环中数字的排列*/
int b[20] = {1};
/*同样count变量记录此问题的解法数量*/
int count = 0;
/*此函数用来排列1-20这20个数,space表示当前排列的是第几个数*/
void numberLend(int space);
/*此函数求解的是当前位置pointi的值valuei是否合理*/
bool isAvailable(int pointi, int valuei);
/*此函数用来求两个数的和是否为素数*/
bool isPrime(int num1, int num2);
int main() {
/*给素数环赋初值,除第一个元素以外,其它元素均为-1*/
for(int i = 1; i < 20; i++)
b[i] = -1;
numberLend(1); /*从第1个位置填数字*/
printf("\n素数环问题的解法count = %d\n", count);
return 0;
}
bool isAvailable(int space, int value) {
if(!isPrime(value, b[space - 1]))
return false;
for(int i = 0; i < space; i++) {
if(value == b[i])
return false;
}
return true;
}
bool isPrime(int num1, int num2) {
int num = num1 + num2;
for(int i = 2; i <= sqrt(num); i++) {
if(num % i == 0)
return false;
}
return true;
}
void numberLend(int space) {
/*在第space位置上填写1-20数字*/
for(int i = 1; i < 21; i++) {
/*遍历第space个位置上的1-20个数字*/
if(isAvailable(space, i)) {
b[space] = i;
/*数字排列完毕*/
if(space == 19) {
/*嘿!*/
/*别忘了还要判断首尾的数字之和是否为素数*/
if(isPrime(b[0], b[19])) {
count++;
printf("<%d> ", count);
for(int j = 0; j < 20; j++) {
printf("%d ", b[j]);
}
printf("\n");
}
else {
b[19] == -1;
}
return;
}
/*处理本次素数环下一个位置*/
int nextSpace = space + 1;
numberLend(nextSpace);
}
}
/*上个位置放置的数字不合理,回溯上一个位置*/
b[--space] = -1;
return;
}
以下是部分运行结果!
4. 小结
关于递归回溯问题,重要的一点就是找到回溯的条件以及递归的出口!!当然,这需要我们平时多加练习,锻炼递归的这种思维模式。
?,今天的分享就先到这,第一次写博客,以上内容有什么问题还请大家随时指正!