递归与回溯算法

  最近在看数据结构时,我发现了很多关于递归回溯的问题,特别是在学习了树和图等非线性结构后,对递归回溯的问题也有了一些更深的认识,但是有时候我们可能不是很能分清楚这二者。那么,接下来我们来一起看看这二者的关系如何呢?
  递归,是一种算法结构,一个递归就是在函数中调用函数本身来解决问题。在描述问题的某一状态时,需要用到该状态的上一状态,而描述上一状态,又需要用到上一状态的上一状态……这种用自已来定义自己的方法,称为递归定义。当然递归也不是一直无休止的自身调用,他总需要有一个递归出口条件。比如最简单的递归算法,求阶乘,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. 小结

  关于递归回溯问题,重要的一点就是找到回溯的条件以及递归的出口!!当然,这需要我们平时多加练习,锻炼递归的这种思维模式。

  ?,今天的分享就先到这,第一次写博客,以上内容有什么问题还请大家随时指正!

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值