《算法笔记》学习日记——4.3 递归

4.3 递归

Codeup Contest ID:100000583

问题 A: 吃糖果

题目描述
名名的妈妈从外地出差回来,带了一盒好吃又精美的巧克力给名名(盒内共有 N 块巧克力,20 > N >0)。
妈妈告诉名名每天可以吃一块或者两块巧克力。
假设名名每天都吃巧克力,问名名共有多少种不同的吃完巧克力的方案。
例如:
如果N=1,则名名第1天就吃掉它,共有1种方案;
如果N=2,则名名可以第1天吃1块,第2天吃1块,也可以第1天吃2块,共有2种方案;
如果N=3,则名名第1天可以吃1块,剩2块,也可以第1天吃2块剩1块,所以名名共有2+1=3种方案;
如果N=4,则名名可以第1天吃1块,剩3块,也可以第1天吃2块,剩2块,共有3+2=5种方案。
现在给定N,请你写程序求出名名吃巧克力的方案数目。
输入
输入只有1行,即整数N。
输出
可能有多组测试数据,对于每组数据,
输出只有1行,即名名吃巧克力的方案数。
样例输入

1
2
4

样例输出

1
2
5

思路
这题的思路非常简单,因为每天只能吃一块或者两块巧克力,所以当N=某个数的时候,方案也只可能是两种相加,那么只要往前递归即可(比如N=4的时候,第一天吃一块剩3块,或者第一天吃一块剩2块,那么只要往前找N=3和N=2的值相加就能得到答案了)。
代码

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<algorithm>
using namespace std;
int eat(int n){
	if(n==1) return 1;
	else if(n==2) return 2;
	else return eat(n-1)+eat(n-2);//吃一块和吃两块两种选择,分别往前找对应的n 
}
int main(){
	int N;
	while(scanf("%d", &N) != EOF){
		printf("%d\n", eat(N));
	} 
	return 0;
} 

问题 B: 数列

题目描述
编写一个求斐波那契数列的递归函数,输入n 值,使用该递归函数,输出如下图形(参见样例)。
输入
输入第一行为样例数m,接下来有m行每行一个整数n,n不超过10。
输出
对应每个样例输出要求的图形(参见样例格式)。
样例输入

1
6

样例输出

          0
        0 1 1
      0 1 1 2 3
    0 1 1 2 3 5 8
  0 1 1 2 3 5 8 13 21
0 1 1 2 3 5 8 13 21 34 55

思路
这题也是比较简单,把斐波那契的递归函数写出来之后,再按照题目要求输出即可。
代码

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<algorithm>
using namespace std;
int fibonacci(int n){
	if(n==0) return 0;//fibonacci(0)=0 
	else if(n==1) return 1; //fibonacci(1)=1
	else return fibonacci(n-1)+fibonacci(n-2);
}
int main(){
	int m;
	while(scanf("%d", &m) != EOF){
		while(m--){
			int n;
			scanf("%d", &n);
			for(int i=0;i<n;i++){
				for(int k=2*(n-i)-2;k>=1;k--) printf(" ");
				for(int j=0;j<2*i+1;j++){
					if(j==0) printf("%d", fibonacci(j));
					else printf(" %d", fibonacci(j));
				}
				printf("\n");
			}
		}
	}
	return 0;
} 

问题 C: 神奇的口袋

题目描述
有一个神奇的口袋,总的容积是40,用这个口袋可以变出一些物品,这些物品的总体积必须是40。John现在有n个想要得到的物品,每个物品的体积分别是a1,a2……an。John可以从这些物品中选择一些,如果选出的物体的总体积是40,那么利用这个神奇的口袋,John就可以得到这些物品。现在的问题是,John有多少种不同的选择物品的方式。
输入
输入的第一行是正整数n (1 <= n <= 20),表示不同的物品的数目。接下来的n行,每行有一个1到40之间的正整数,分别给出a1,a2……an的值。
输出
输出不同的选择物品的方式的数目。
样例输入

2
12
28
3
21
10
5

样例输出

1
0

思路
因为想了好久没想出来可行的方案,一开始是想遍历所有子集然后求和,最后计算等于40的子集个数,但是这样子的话只要元素一多,子集的数量是指数型增加的(2n-1),所以显然是不可行的。然后就想不通该如何设计一种递归算法让它找出每种可行的方案-_-||,后来看了大佬的博客才恍然大悟,原来和问题A差不多……

思路是借鉴了ActionBeam的方法,从最后一个元素往前推,和问题A类似,问题A中第一天有吃1块或者吃2块两种选择,那么这里也是一样,第一次有包含最后一个元素和不包含最后一个元素两种选择,如果包含最后一个元素,那么前面的东西只要能凑到(40-最后一个元素)的量即可,而如果不包含,那么前面的东西就得凑到40,这样,递推公式就出来了:goods(n, v)=goods(n-1, v)+goods(n-1, v-(temp[n])),至于递归的边界就是:①v=0,说明已经完整凑出一个40来,就return 1;②n<=0,说明已经倒头了但是依然没有凑出来,就return 0。
代码

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<algorithm>
using namespace std;
int temp[21]={0};//存放John想要的东西 
int goods(int n, int v){//v就是口袋的容积
	if(v==0) return 1;//当递归到v=0时,说明产生了一种可行方案,return1
	if(n<=0) return 0;//当递归到n<=0时,说明已经到尽头但仍未产生可行方案,return0
	else return goods(n-1,v)+goods(n-1,v-(temp[n])); 
}
int main(){
	int n;
	while(scanf("%d", &n) != EOF){
		for(int i=1;i<=n;i++) scanf("%d", &temp[i]);
		printf("%d\n", goods(n,40));
		memset(temp, 0, sizeof(temp));
	}
	return 0;
} 

问题 D: 八皇后

题目描述
会下国际象棋的人都很清楚:皇后可以在横、竖、斜线上不限步数地吃掉其他棋子。如何将8个皇后放在棋盘上(有8 * 8个方格),使它们谁也不能被吃掉!这就是著名的八皇后问题。
对于某个满足要求的8皇后的摆放方法,定义一个皇后串a与之对应,即a=b1b2…b8,其中bi为相应摆法中第i行皇后所处的列数。已经知道8皇后问题一共有92组解(即92个不同的皇后串)。
给出一个数b,要求输出第b个串。串的比较是这样的:皇后串x置于皇后串y之前,当且仅当将x视为整数时比y小。
输入
第1行是测试数据的组数n,后面跟着n行输入。每组测试数据占1行,包括一个正整数b(1 <= b <= 92)
输出
输出有n行,每行输出对应一个输入。输出应是一个正整数,是对应于b的皇后串。
样例输入

3
6
4
25

样例输出

25713864
17582463
36824175

思路
这题是n皇后问题,书上也有很详细的递归函数的代码。主要的想法就是把行号和列号用一个数组对应起来(因为每行每列只有一个皇后),然后再进行全排列(全排列用递归函数写),把两个皇后在对角线的情况(行号之差等于列号之差)剔除即可。因为全排列本身就是从小到大有序的,所以不需要再进行排序。
代码

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<algorithm>
using namespace std;
struct str{
	int s;
}tmp[93];
int p[9]={0};//下标代表行号(1~8),其值代表列号 
bool hashtable[9];
int cnt = 0;//记录方案数 
void queen(int index){//8皇后 
	if(index==9){//完成一种方案 
		cnt++;
		//把数组p中的元素填入结构体数组tmp(用二维数组也行)中:
		for(int i=1, j=7;i<=8, j>=0;i++, j--) tmp[cnt].s += p[i]*pow(10,j);//把p[1]~p[8]的元素转换成整数再给tmp的属性s 
		return;
	}
	for(int x=1;x<=8;x++){//x是列号,index是行号 
		if(hashtable[x]==false){
			bool flag = true;
			for(int pre=1;pre<index;pre++){
				if(abs(index-pre) == abs(x-p[pre])){//如果行的差值等于列的差值,说明在一条对角线上 
					flag = false;
					break;//回溯,不再往下递归 
				}
			}
			if(flag){//否则正常进行递归操作 
				p[index] = x;
				hashtable[x] = true;
				queen(index+1);
				hashtable[x] = false;
			}
		}
	}
}
int main(){
	int n;
	queen(1);
	while(scanf("%d", &n) != EOF){
		while(n--){
			int b;
			scanf("%d", &b);
			printf("%d\n", tmp[b].s);
		}
	}
	return 0;
} 

小结

首先,递归对我来说是一个比较难理解的东西,包括递归的想法和递归代码的编写。昨天那题神奇的口袋我一直想到了后半夜,因为往往在思考的时候,我们会习惯性地沿着递归的路一直往最底层走,一直到递归边界再一步步回来,但是这样子在数据规模小、问题简单的时候还好,当数据规模大一点、问题复杂一点的时候,根本思考不清楚。
昨天我也看了极客时间上王争老师的《数据结构与算法之美》中的递归章节,就像老师说的:

计算机擅长做重复的事情,所以递归正和它的胃口。而我们人脑更喜欢平铺直叙的思维方式。当我们看到递归时,我们总想把递归平铺展开,脑子里就会循环,;一层一层往下调,然后再一层一层返回,试图想搞清楚计算机每一步是怎么执行的,这样就很容易被绕进去

而思考递归的正确打开方式是:

如果一个问题A可以分解为若干子问题B、C、D,你可以假设子问题B、C、D已经解决,在次基础上思考如何解决问题A。而且,你只需要思考问题A与子问题B、C、D两层之间的关系即可

这对我来说是受益匪浅的,就比如说昨天思考了很久问题C神奇口袋,其实没必要把每一步都想完,然后再转化成递归代码,只要看第一步和第二步之间的关系就好了,而第一步只有两种选择:要么取到最后一个值,加上前面的一起凑成容积40,要么取不到最后一个值,前面的加在一起要凑成容积40,这样想的话,递归就非常好写了(这和问题A是如出一辙的)。

又比如八皇后全排列的问题,昨天在学习递归章节的时候,确实花了大量的时间去理解全排列的递归函数,基本上把那一页都写满了(我把递归的每一层都想了一遍),后来看到了这句话,我就试着只分析generateP(index)和generateP(index+1)之间的关系,显然,如果对{1,2,3}全排列,在for循环中,先将P[1]置1的话,假设子问题generateP(index+1)已解决,就意味着{1,2,3}和{1,3,2}已经排列完毕,那么把hashtable[1]的值置false(把1拿出来),下一层for循环,将P[1]置2,再排列出{2,1,3}和{2,3,1}也就很好理解了。

总的来说,递归还是要多想多练,不然遇到问题还是会想不明白的_(:з」∠)_。

发布了54 篇原创文章 · 获赞 27 · 访问量 5000

猜你喜欢

转载自blog.csdn.net/weixin_42257812/article/details/105008132