算法与程序实践习题解答8递归.docx
- 文档编号:23037035
- 上传时间:2023-04-30
- 格式:DOCX
- 页数:22
- 大小:46.04KB
算法与程序实践习题解答8递归.docx
《算法与程序实践习题解答8递归.docx》由会员分享,可在线阅读,更多相关《算法与程序实践习题解答8递归.docx(22页珍藏版)》请在冰豆网上搜索。
算法与程序实践习题解答8递归
《算法与程序实践》习题解答8——递归
让我们来看看计算n的阶乘的计算机程序的写法,很直接地我们会用一个循环语句将n以下的数都乘起来:
intn,m=1;
for(inti=2;i<=n;i++)m*=i;
printf(“%d的阶乘是%d\n”,n,m);
因为n的阶乘定义为n乘以n-1的阶乘,所以还可以用下面的方法来求n的阶乘:
intfactorial(intn){
if(n<=0)return(-1);
if(n==1)return1;
elsereturnn*factorial(n-1);
}
上面这两种实现方式体现了两种不同的解决问题的思想方法。
第一种通过一个循环语句来计算阶乘,其前提是了解阶乘的计算过程,并用语句把这个计算过程模拟出来。
第二种解决问题的思想是不直接找到计算n的阶乘的方法,而是试图找到n的阶乘和n-1的阶乘的递推关系,通过这种递推关系把原来问题缩小成一个更小规模的同类问题,并延续这一缩小规模的过程,直到在某一规模上,问题的解是已知的。
这样一种解决问题的思想我们称为递归的思想。
递归方法的总体思想是将待求解问题的解看作输入变量x的函数f(x),通过寻找函数g,使得f(x)=g(f(x-1)),并且已知f(0)的值,就可以通过f(0)和g求出f(x)的值。
这样一个思想也可以推广到多个输入变量x,y,z等,x-1也可以推广到x-x1,只要递归朝着出口的方向走就可以了。
CS81:
菲波那契数列
(来源:
2753,程序设计导引及在线实践(李文新)例9.2P198)
问题描述:
菲波那契数列是指这样的数列:
数列的第一个和第二个数都为1,接下来每个数都等于前面2个数之和。
给出一个正整数a,要求菲波那契数列中第a个数是多少。
输入:
第1行是测试数据的组数n,后面跟着n行输入。
每组测试数据占1行,包括一个正整数a(1<=a<=20)。
输出:
输出有n行,每行输出对应一个输入。
输出应是一个正整数,为菲波那契数列中第a个数的大小。
样例输入:
4
5
2
19
1
样例输出:
5
1
4181
1
解题思路:
这个题目要求很明确,因为a的规模很小,所以递归调用不会产生栈溢出的问题。
设第n项值为f(n),则f(n)=f(n-1)+f(n-2)。
已知f
(1)=1,f
(2)=1,则从第3项开始,可以用公式求。
参考程序:
#include
intf(inta)
{
if(a==1||a==2)return1;
returnf(a-1)+f(a-2);
}
intmain()
{
intn;
inti;
scanf("%d",&n);
for(i=1;i<=n;i++)
{
inta;
scanf("%d",&a);
printf("%d\n",f(a));
}
return0;
}
CS82:
二叉树
(来源:
2756,程序设计导引及在线实践(李文新)例9.3P199)
问题描述:
图8-1满二叉树
如图8-1所示,由正整数1,2,3,...组成了一棵无限大的二叉树。
从某一个结点到根结点(编号是1的结点)都有一条唯一的路径,比如从10到根结点的路径是(10,5,2,1),从4到根结点的路径是(4,2,1),从根结点1到根结点的路径上只包含一个结点1,因此路径就是
(1)。
对于两个结点x和y,假设他们到根结点的路径分别是(x1,x2,...,1)和(y1,y2,...,1)(这里显然有x=x1,y=y1),那么必然存在两个正整数i和j,使得从xi和yj开始,有xi=yj,xi+1=yj+1,xi+2=yj+2,...现在的问题就是,给定x和y,要求xi(也就是yj)。
输入:
输入只有一行,包括两个正整数x和y,这两个正整数都不大于1000。
输出:
输出只有一个正整数xi。
样例输入:
104
样例输出:
2
解题思路:
这个题目要求树上任意两个节点的最近公共子节点。
分析这棵树的结构不难看出,不论奇数偶数,每个数对2做整数除法,就走到它的上层结点。
我们可以每次让较大的一个数(也就是在树上位于较低层次的节点)向上走一个结点,直到两个结点相遇。
如果两个节点位于同一层,并且它们不相等,可以让其中任何一个先往上走,然后另一个再往上走,直到它们相遇。
设common(x,y)表示整数x和y的最近公共子节点,那么,根据比较x和y的值,我们得到三种情况:
1)x与y相等,则common(x,y)等于x并且等于y;2)x大于y,则common(x,y)等于common(x/2,y);3)x大于y,则common(x,y)等于common(x,y/2);
参考程序:
#include
intcommon(intx,inty)
{
if(x==y)returnx;
if(x>y)returncommon(x/2,y);
returncommon(x,y/2);
}
intmain()
{
intm,n;
scanf("%d%d",&m,&n);
printf("%d\n",common(m,n));
return0;
}
注意事项:
问题一:
有一种比较直观的解法是对于两个给定的数,分别求出它们到根节点的通路上的所有节点的值,然后再在两个数组中寻找数码最大的公共节点。
这种做法的代码比较繁琐,容易在实现中出错;
问题二:
代码实现逻辑不明晰,造成死循环等错误,例如,有人只将其中一个数不停地除以2,而不理会另外一个数。
CS83:
逆波兰表达式
(来源:
2694,程序设计导引及在线实践(李文新)例9.4P201)
问题描述:
逆波兰表达式是一种把运算符前置的算术表达式,例如普通的表达式2+3的逆波兰表示法为+23。
逆波兰表达式的优点是运算符之间不必有优先级关系,也不必用括号改变运算次序,例如(2+3)*4的逆波兰表示法为*+234。
本题求解逆波兰表达式的值,其中运算符包括+-*/四个。
输入:
输入为一行,其中运算符和运算数之间都用空格分隔,运算数是浮点数。
输出:
输出为一行,表达式的值。
可直接用printf("%f\n",v)输出表达式的值v。
样例输入:
*+11.012.0+24.035.0
样例输出:
1357.000000
解题思路:
这个问题看上去有些复杂,如果只是简单地模拟计算步骤不太容易想清楚,但是如果用递归的思想就非常容易想清楚。
让我们根据逆波兰表达式的定义进行递归求解。
在递归函数中,针对当前的输入,有五种情况:
1)输入是常数,则表达式的值就是这个常数;2)输入是’+’,则表达式的值是再继续读入两个表达式并计算出它们的值,然后将它们的值相加;3)输入是’-’;4)输入是’*’;5)输入是’/’;后几种情况与2)相同,只是计算从’+’变成’-’,’*’,’/’。
参考程序:
#include
#include
#include
doubleexp()
{
chara[10];
scanf("%s",a);
switch(a[0])
{
case'+':
returnexp()+exp();
case'-':
returnexp()-exp();
case'*':
returnexp()*exp();
case'/':
returnexp()/exp();
default:
returnatof(a);
}
}
intmain()
{
doubleans;
ans=exp();
printf("%f",ans);
return0;
}
CS84:
放苹果
(来源:
1664,程序设计导引及在线实践(李文新)例题9.5P203)
问题描述:
把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法(用K表示)?
注意:
5,1,1和1,5,1是同一种分法。
输入:
第一行是测试数据的数目t(0<=t<=20)。
以下每行均包含二个整数M和N,以空格分开。
1<=M,N<=10。
输出:
对输入的每组数据M和N,用一行输出相应的K。
样例输入:
1
73
样例输出:
8
解题思路:
所有不同的摆放方法可以分为两类:
至少有一个盘子空着和所有盘子都不空。
我们可以分别计算这两类摆放方法的数目,然后把它们加起来。
对于至少空着一个盘子的情况,则N个盘子摆放M个苹果的摆放方法数目与N-1个盘子摆放M个苹果的摆放方法数目相同。
对于所有盘子都不空的情况,则N个盘子摆放M个苹果的摆放方法数目等于N个盘子摆放M-N个苹果的摆放方法数目。
我们可以据此来用递归的方法求解这个问题。
设f(m,n)为m个苹果,n个盘子的放法数目,则先对n作讨论,如果n>m,必定有n-m个盘子永远空着,去掉它们对摆放苹果方法数目不产生影响;即if(n>m)f(m,n)=f(m,m)。
当n<=m时,不同的放法可以分成两类:
即有至少一个盘子空着或者所有盘子都有苹果,前一种情况相当于f(m,n)=f(m,n-1);后一种情况可以从每个盘子中拿掉一个苹果,不影响不同放法的数目,即f(m,n)=f(m-n,n)。
总的放苹果的放法数目等于两者的和,即f(m,n)=f(m,n-1)+f(m-n,n)。
整个递归过程描述如下:
intf(intm,intn){
if(n==1||m==0)return1;
if(n>m)returnf(m,m);
returnf(m,n-1)+f(m-n,n);
}
出口条件说明:
当n=1时,所有苹果都必须放在一个盘子里,所以返回1;当没有苹果可放时,定义为1种放法;递归的两条路,第一条n会逐渐减少,终会到达出口n==1;第二条m会逐渐减少,因为n>m时,我们会returnf(m,m)所以终会到达出口m==0。
参考程序:
#include
intcount(intx,inty)
{
if(y==1||x==0)return1;
if(x returncount(x,y-1)+count(x-y,y); } intmain() { intt,m,n; inti; scanf("%d",&t); for(i=0;i { scanf("%d%d",&m,&n); printf("%d\n",count(m,n)); } return0; } 注意事项: 问题一: 没有想清楚如何递归,用循环模拟逐一枚举的做法时考虑不周出错; 问题二: 出口条件判断有偏差,或者没有分析出当盘子数大于苹果数时要处理的情况; CS85: 红与黑 (来源: 2816,程序设计导引及在线实践(李文新)例9.6P204) 问题描述: 有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。 你站在其中一块黑色的瓷砖上,只能向相邻的黑色瓷砖移动。 请写一个程序,计算你总共能够到达多少块黑色的瓷砖。 输入: 包括多个数据集合。 每个数据集合的第一行是两个整数W和H,分别表示x方向和y方向瓷砖的数量。 W和H都不超过20。 在接下来的H行中,每行包括W个字符。 每个字符表示一块瓷砖的颜色,规则如下 1)‘.’: 黑色的瓷砖; 2)‘#’: 红色的瓷砖; 3)‘@’: 黑色的瓷砖,并且你站在这块瓷砖上。 该字符在每个数据集合中唯一出现一次。 当在一行中读入的是两个零时,表示输入结束。 输出: 对每个数据集合,分别输出一行,显示你从初始位置出发能到达的瓷砖数(记数时包括初始位置的瓷砖)。 样例输入: 69 ....#. .....# ...... ...... ...... ...... ...... #@...# .#..#. 00 样例输出: 45 解题分析: 这个题目可以描述成给定一点,计算它所在的连通区域的面积。 需要考虑的问题包括矩阵的大小,以及从某一点出发向上下左右行走时,可能遇到的三种情况: 出了矩阵边界、遇到’.’、遇到’#’。 设f(x,y)为从点(x,y)出发能够走过的黑瓷砖总数,则f(x,y)=1+f(x-1,y)+f(x+1,y)+f(x,y-1)+f(x,y+1) 这里需要注意,凡是走过的瓷砖不能够被重复走过。 可以通过每走过一块瓷砖就将它作标记的方法保证不重复计算任何瓷砖。 参考程序: #include intW,H; charz[21][21]; intf(intx,inty) { if(x<0||x>=W||y<0||y>=H)//如果走出矩阵范围 return0; if(z[x][y]=='#')//该块瓷砖已被走过 return0; else { z[x][y]='#'; return1+f(x-1,y)+f(x+1,y)+f(x,y-1)+f(x,y+1); } } intmain() { inti,j,num; while(scanf("%d%d",&H,&W)&&W! =0&&H! =0) { num=0; for(i=0;i scanf("%s",z[i]); for(i=0;i for(j=0;j if(z[i][j]=='@') printf("%d\n",f(i,j)); } return0; } 注意事项: 问题一: 走过某块瓷砖后没有将它标记,导致重复计算或无限递归; 问题二: 在递归出口条件判断时,先判断该网格点是否是’#’,再判断是否出边界,导致数组越界; 问题三: 读入数据时,用scanf一个字符一个字符读入,没有去掉数据中的行尾标记,导致数据读入出错。 在上面放苹果的例题中可以看出,在寻找从f(x)向出口方向的递归方法时,我们是对可能的情况做了一步枚举,即将所有可能情况划分为至少有一个盘子空着和所有盘子至少有一个苹果两种情况。 这种通过一步枚举进行递归的方法是很常用的。 例如在例题“红与黑”中,我们枚举了在一个方格点上的四种可能的走法。 例题“红与黑”与前几个例题不同的地方在于,在该问题中有一个记录地图的全局量,在每一个格点行走时,我们会改变这个全局量的状态。 我们在处理每个格点时按上下左右的顺序依次走向相邻格点,当我们走过左边的格点时,改变了全局量的状态,只是这种改变不影响我们继续走向右边的格点。 但是对于另外一类问题,情况可能会不同,在我们尝试了前面的分支情况后,要将全局量恢复成进入分支前的状态,然后再尝试其它的分支情况。 下面的例题就是这种情况。 CS86: 八皇后问题 (来源: 2754,程序设计导引及在线实践(李文新)例9.7P207) 问题描述: 会下国际象棋的人都很清楚: 皇后可以在横、竖、斜线上不限步数地吃掉其他棋子。 如何将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的皇后串。 样例输入: 2 1 92 样例输出: 15863724 84136275 数据分析: 解题分析 (1): 因为要求出92种不同摆放方法中的任意一种,所以我们不妨把92种不同的摆放方法一次性求出来,存放在一个数组里。 为求解这道题我们需要有一个矩阵仿真棋盘,每次试放一个棋子时只能放在尚未被控制的格子上,一旦放置了一个新棋子,就在它所能控制的所有位置上设置标记,如此下去把八个棋子放好。 当完成一种摆放时,就要尝试下一种。 若要按照字典序将可行的摆放方法记录下来,就要按照一定的顺序进行尝试。 也就是将第一个棋子按照从小到大的顺序尝试;对于第一个棋子的每一个位置,将第二个棋子从可行的位置从小到大的顺序尝试;在第一第二个棋子固定的情况下,将第三个棋子从可行的位置从小到大的顺序尝试;依次类推。 首先,我们有一个8*8的矩阵仿真棋盘标识当前已经摆放好的棋子所控制的区域。 用一个有92行每行8个元素的二维数组记录可行的摆放方法。 用一个递归程序来实现尝试摆放的过程。 基本思想是假设我们将第一个棋子摆好,并设置了它所控制的区域,则这个问题变成了一个7皇后问题,用与8皇后同样的方法可以获得问题的解。 那我们就把重心放在如何摆放一个皇后棋子上,摆放的基本步骤是: 从第1到第8个位置,顺序地尝试将棋子放置在每一个未被控制的位置上,设置该棋子所控制的格子,将问题变为更小规模的问题向下递归,需要注意的是每次尝试一个新的未被控制的位置前,要将上一次尝试的位置所控制的格子复原。 参考程序 (1): #include #include intqueenPlaces[92][8];//存放92种皇后棋子的摆放方法 intcount=0;//记录当前棋子摆放成功方案的个数 intboard[8][8];//仿真棋盘 voidputQueen(intithQueen);//递归函数,每次摆好一个棋子 intabs(inta)//G++里没有这个函数,需要加 { if(a<0) return-1*a; returna; } intmain() { intn,i,j; for(i=0;i<8;i++)//初始化 { for(j=0;j<8;j++) board[i][j]=-1; for(j=0;j<92;j++) queenPlaces[j][i]=0; } putQueen(0);//从第0个棋子开始摆放,运行的结果是将queenPlaces生成好 scanf("%d",&n); for(i=0;i { intith; scanf("%d",&ith); for(j=0;j<8;j++) printf("%d",queenPlaces[ith-1][j]); printf("\n"); } return0; } voidputQueen(intithQueen) { inti,k,r; if(ithQueen==8) { count++; return; } for(i=0;i<8;i++) { if(board[i][ithQueen]==-1) { //找到可以摆放皇后的位置,摆放皇后 board[i][ithQueen]=ithQueen; //将其后所有的摆放方法的第ith个皇后都放在i+1的位置上, //在i增加以后,后面的第ith个皇后摆放方法会覆盖此时的设置 for(k=count;k<92;k++) queenPlaces[k][ithQueen]=i+1; //设置控制范围 for(k=0;k<8;k++) for(r=0;r<8;r++) if(board[k][r]==-1&&(k==i||r==ithQueen||abs(k-i)==abs(r-ithQueen))) board[k][r]=ithQueen; //向下级递归 putQueen(ithQueen+1); //回溯,撤销控制范围 for(k=0;k<8;k++) for(r=0;r<8;r++) if(board[k][r]==ithQueen) board[k][r]=-1; } } } 解题分析2: 上面的方法用一个二维数组来记录棋盘被已经放置的棋子的控制情况,每次有新的棋子放置时用了枚举法来判断它控制的范围。 还可以用三个一维数组来分别记录每一列,每个45度的斜线和每个135度的斜线上是否已经被已放置的棋子控制,这样每次有新的棋子放置时,不必再搜索它的控制范围,可以直接通过三个一维数组判断它是否与已经放置的棋子冲突,在不冲突的情况下,也可以通过分别设置三个一维数组的相应的值,来记录新棋子的控制范围。 参考程序2: #include intrecord[92][9],mark[9],count=0;//record记录全部解,mark记录当前解 boolrange[9],line1[17],line2[17];//分别记录列方向,45度,135度方向上被控制的情况 voidtryToPut(int);//求全部解的过程 intmain() { inti,testtimes,num; scanf("%d",&testtimes); for(i=0;i<=8;i++) range[i]=true; for(i=0;i<17;i++) line1[i]=line2[i]=true; tryToPut (1); while(testtimes--) { scanf("%d",&num); for(i=1;i<=8;i++) printf("%d",record[num-1][i]); printf("\n"); } return0; } voidtryToPut(inti) { intj,k; if(i>8)//如果最后一个皇后被放置完毕,将当前解复制到全部解中相应位置 { for(k=1;k<9;k
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 算法 程序 实践 习题 解答 递归
![提示](https://static.bdocx.com/images/bang_tan.gif)