1、第五讲 递推与递归,主要内容递推及其应用递归及其应用,第五讲 递推与递归,递推是通过数学推导,将复杂的运算化解为若干重复的简单运算,以充分发挥计算机擅长重复处理的特点。递归是将复杂的操作分解为简单操作的多次重复,一般用函数调用完成。,递推关系是一种简洁高效的常见数学模型。 如:Fibonacci数列问题。递推特点:在递推问题中,每个数据项都和它前面的若干个数据项(或后面的若干个数据项)有一定的关联,这种关联一般通过“递推关系式”表示。,11.1 递 推,递推过程:从初始的一个或若干个数据项出发,通过递推关系式逐步推进,从而得到最终结果。其中:初始的若干数据项称为“边界”。,11.1 递 推,1
2、1.1 递 推,例如:Fibonacci数列问题 已知:F (1) = 0 ,F(2) = 1, 若: n2,则F(n)= F(n-1)+F(n-2)注意: 1)每个数据项都和它前面的若干个数据项(或后面的若干个数据项)有一定的关联-递推关系式。 2)从初始的一个或若干数据项出发,通过递推关系式逐步推进,从而得到最终结果。,11.1 递 推,注意:3)递推必须有 “边界”。4)解决递推类型问题有三个重点: 如何建立正确的递推关系式; 递推关系有何性质; 递推关系式如何求解。,基础,重点,11.1 递 推,按照推导问题的方向,递推分为:顺推法:从初始数据开始推理。 例如:n=0时,n!=1; n
3、1时,n!=n*(n-1)!倒推法:从结果数据开始推理。 例如:n!=n*(n-1)! ; 边界:n=0,n!=1,例1:猴子第1天摘下若干个桃子,当即吃了一半又一个。第2天又把剩下的桃吃了一半又一个,以后每天都吃前一天剩下的桃子的一半又一个,到第10天猴子想吃时,只剩下一个桃子。问:猴子第1天一共摘了多少桃子?,分析:已知条件:第 10 天剩下 1 个桃子;隐含条件:每一次前一天的桃子个数等于后一天桃子的个数加 1 的 2 倍。逆向思维:从后往前推,可用倒推法求解。,#include void main( )int i,a=1;for (i=9; i=1; i-) a=(a+1)*2;pri
4、ntf(a=%dn,a);,例1:逆推法-求解猴子吃桃子,例2:猴子分食桃子1) 五只猴子采得一堆桃子,猴子彼此约定隔天早起后再分食。2) 半夜里,一只猴子偷偷起来,把桃子均分成五堆后,发现还多一个,它吃掉这桃子,并拿走了其中一堆。3)第二只猴子醒来,又把桃子均分成五堆后,还是多了一个,它也吃掉这个桃子,并拿走了其中一堆。第三只,第四只,第五只猴子都依次如此分食桃子。问:五只猴子采得一堆桃子数最少应该有几个呢?,逆推法,算法分析: 先要找第N只猴子和其面前桃子数的关系。如果从第1只开始往第5只找,不好找,但如果思路一变,从第N到第1去,可得出下面的推导式: 第N只猴 第N只猴前桃子数目5 s5
5、=x4 s4=s5*5/4+13 s3=s4*5/4+12 s2=s3*5/4+11 s1=s2*5/4+1通过递推,求出s1即可,例2:猴子分食桃子-逆推法,注意:其中的s1、s2、s3、s4、s5必须是整数,/例2:猴子分食桃子-逆推法#include void main()int x,s,k,i;x=6; / 最少的初值k=0; / 整除标志while ( k!=4)s=x;k=0;for ( i=4; i=1; i-) if ( s%4 = 0) k+; else break; s=s*5/4+1; x=x+5;printf(s=%dn,s);,11.1.2 递推设计实例,例11.1:
6、母牛的故事 问题描述:有一头母牛,它每年年初生一头小母牛。每头小母牛从第四个年头开始,每年年初也生一头小母牛。编程实现:在第n年的时候,共有多少头母牛?,例11.1:母牛的故事-顺推法,设数组f(i)表示第 i 年的母牛总数,则第 n 年的母牛总数为f(n) 。 f(n) 与两个值有关 在本年之前就已经出生的母牛数目 在本年新出生的小母牛数目。1) 本年之前就已经出生的母牛数目,实际上就是前一年的母牛总数-f(n-1)。2) 由于每一头母牛每年只生育一头小母牛,所以在本年新出生的小母牛数目,实际上是到今年可以生育的母牛的数目。3) 而每头小母牛从第四个年头才开始生育,所以今年可以生育的母牛的数
7、实际上就是三年前的母牛总数,即为f(n-3)。,例11.1:母牛的故事-顺推法,递推公式: f(n)=f(n-1)+f(n-3) (n=4)第一、二、三年的母牛总数是直接可知的: f (1) =1; f (2) =2; f (3) =3;,递推边界,也可以从最简单开始,找规律: 若数组f(i)表示第 i 年的母牛总数,则第 n 年的母牛总数为f(n) 。 n=1 f(n)=1 n=2 f(n)=2 n=3 f(n)=3 n=4 f(n)=3 + 1 =f3+ f1=4 n=5 f(n)=f(4)+f(2) n=6 f(n)=f(5)+f(3) n=7 f(n)=f(6)+f(4) 规律: f(
8、n)=f(n-1)+f(n-3) (n=4),例11.1:母牛的故事-顺推法,#include void main() _int64 f50; / _int64 - 定义大整数 int i,n; scanf(%d, / %I64d大整数输入/输出格式 ,问题描述:在2n的长方形方格中,用n个12的骨牌铺满方格。问:输入n ,输出铺放方案的总数?例如:n=3时,有23方格 骨牌的铺放方案有三种方法,如下图所示:,例11.2 骨牌问题-顺推法,长度为n时的骨牌铺放方案?从最简单的情况开始寻找问题解决的规律?- 顺推以 f(i) 表示n=i时的铺放方案数目。当n=1时,只有1种铺法,即f(1)=1,
9、如下左图所示:当n=2时,只有2种铺法,即f(2)=2,如下右图所示。,例11.2 骨牌问题-顺推法,n=3时骨牌的铺放方案有3种方法,f(3)=3如下图:,例11.2 骨牌问题-顺推法,这3种铺放方法可以采用如下的步骤分析得到:n=3时,第一块骨牌的铺法只有2种可能,横铺或者竖铺,即:(1)横铺方式:在第一格横放一个骨牌,此时剩余两格,在两格内铺放骨牌有f(2)种铺法;(2)竖铺方式:在第一、二格竖放两个骨牌,此时剩余一格,在一格内铺放骨牌有f(1)种铺法; f(3)=f(2)+f(1)=2+1=3。,对于一般的n值,其第一块骨牌的铺法也只有两种可能,横铺或者竖铺:,例11.2 骨牌问题-顺
10、推法,(1)横铺方式:若第一格横放一个骨牌,此时剩余n-1格,在n-1格放n-1个骨牌有f(n-1)种铺法;(2)竖铺方式:若第一、二格竖放两格骨牌,此时剩余n-2格,在n-2格放n-2个骨牌有f(n-2)种铺法;,因此,n块骨牌的铺法是首块骨牌横铺方式的铺法与竖铺方式的铺法之和。 即: f(n)=f(n-1)+f(n-2)边界: f(1)=1 ,f(2)=2,例11.2 骨牌问题-顺推法,递推公式,#includeint main() int i,n, f20; f0=1; f1=2; / 边界 scanf(%d, ,例11.2 骨牌问题,#includevoid main() int i,
11、n; _int64 f50; f0=1; f1=2; / 边界 scanf(%d, ,例11.2 骨牌问题-顺推法,问题描述: 设有一个共有n级的楼梯,某人每步可走1级,也可走2级,也可走3级。问:求从底层开始走完全部楼梯的有多少种走法。例如,当n=3时,走法如下: 1+1+1 1+2 2+1 3,思考:上楼问题,n=3,共有4种走法,n的值 走法f(n) 1 1 2 2 3 4 4 7从递推的思想出发,可以设想,从第4项开始,每1项等于前面3项的和。,上楼问题-算法分析,f(n)=f(n-1)+f(n-2)+f(n-3)-递推公式 f(1)=1 , f(2)=2 , f(3)=4 -递推边界
12、,#include /上楼问题void main( ) int x,n,i,a,b,c;scanf(%d,例11.3 错排信件,问题描述:某人写了n封信和n个信封,所有的信都装错了信封。要求:若所有的信都装错信封,共有多少种不同情况?,例11.3 错排信件,找规律:对n封信以及n个信封各自按照从 1.n 进行编号; f(n)-n个编号的信放在n个编号的信封,各不对应的方法数;f(n-1)-n-1个编号的信放在n-1个编号位置的信封,各不对应的方法数。,例11.3 错排信件,找规律:第一步: 把第n封信放在第k个信封(kn),不对应的共有n-1种方法;第二步: 放编号为 k 的信,有两种情况:
13、1)放编号为 k 的信放到第n个信封,则对于剩下的n-2封信,需要放到剩余的n-2个信封且各不对应,就有f(n-2)种方法; 2)放编号为 k 的信不放到位置n,对于这n-1封信,放到剩余的n-1个信封且各不对应,有f(n-1)种方法; 结论: f(n) = (n-1) * ( f (n-2) + f (n-1) ) 特例:f(1)= 0, f(2)= 1 -边界,例11.3 错排信件,#includevoid main() int i,n, f20; f1=0;f2=1; scanf(%d, ,例11.4 马踏过河卒-选讲棋盘上A点有一个过河卒,需要走到目标B点。卒行走的规则:可以向下、或者
14、向右。同时在棋盘上的任一点有一个对方的马(如下图中的C点),该马所在的点和所有跳跃一步可达的点称为对方马的控制点(如下图中的C点和P1,P2,P8)。卒不能通过对方马的控制点。棋盘用坐标表示,A点(0,0)、B点(n, m) (n,m为不超过20的整数),同样马的位置坐标是需要给出的,CA且CB。编程:输入n,m,计算出卒从A点能够到达B点的路径的条数。,例11.4 马踏过河卒-选讲,马踏过河卒是一道很老的题目,一些程序设计比赛中也经常出现过这一问题的变形。一看到这种类型的题目容易让人想到用搜索来解决,但盲目的搜索仅当n,m=15就会超时。可以试着用递推来进行求解。根据卒行走的规则,过河卒要到
15、达棋盘上的一个点,只能有两种可能:从左边过来(左点)或是从上面过来(上点),所以根据加法原理,过河卒到达某一点的路径数目,就等于其到达其相邻的上点和左点的路径数目之和,因此可用逐列(或逐行)递推的方法求出从起点到终点的路径数目。障碍点(马的控制点)也完全适用,只要将到达该点的路径数目设置为0即可。,例11.4 马踏过河卒-选讲,用二维数组元素fij表示到达点(i,j)的路径数目;用gij表示点(i,j)是否是对方马的控制点;若gij=0表示不是对方马的控制点,gij=1表示是对方马的控制点。则可以得到如下的递推关系式: fij = 0 当 gij=1 fi0 = fi-10 当 i0, gij
16、=0 f0j = f0j-1 当 j0, gij=0 fij = fi-1j + fij-1 当 i0, j0, gij=0 递推边界:f00 = 1,#include / 马踏过河卒 int main() int i,j,n,m,f2020,g2020,x,y; scanf(%d %d %d %d,for (i=1;i=n;i+)if(gi0!=1) fi0=1;else for (;i=n;i+)fi0=0;for (j=1;j=m;j+)if(g0j!=1) f0j=1;else for(;j=m;j+) f0j=0;for (i=1;i=n;i+) for (j=1;j=m;j+)if
17、 (gij=0) fij=fi-1j+fij-1;printf(%dn,fnm);return 0;,例11.4 马踏过河卒改进,为了更简洁地表示马的控制点,可以引入两个一维数组,分别用来统计从马的初始位置可以横向移动的位移与纵向移动的位移。程序的改进如下:#includeint main() int dx9=0,-2,-1,1,2,2,1,-1,-2; int dy9=0,1,2,2,1,-1,-2,-2,-1; int n,m,x,y,i,j; int f2020=0,g2020=0; scanf(%d%d%d%d,例11.4 马踏过河卒,for(i=1;i=0),递推应用:王小二切饼,要
18、求每2条线都有交点。,问:切第n刀后,饼被分为多少块?,找规律:王小二切饼,要求每2条线都有交点。,n=0; p(0)=1; n=1; p(1)=2; n=2; p(2)=4;,n=3; p(3)=7;n=4; p(4)=11;,规律:p(n)=p(n-1)+n,#include / 用递推求解-切饼问题int main()int n,i; int p100; p0=1;scanf(%d,递推思考输出杨晖三角形的前10行。杨晖三角形的前5行如左下图所示。,问题分析:观察左上图不太容易找到规律,但如果将左上图转化为右上图就不难发现杨辉三角形其实就是一个二维表(数组)的下三角部分。,#includ
19、e /打印杨辉三角形void main() int i,j,a1010; for (i=0;i10;i+) ai0=1; for (i=0;i10;i+) for (j=i+1;j10;j+) aij=0; for (i=1;i10;i+) for (j=1;j=i;j+) aij=ai-1j-1+ai-1j; for (i=0;i10;i+) for (j=0;j Y;2# 青蛙从 L S;1# 青蛙从 Y S;3# 青蛙从 L Y;4# 青蛙从 L R;,左岸L,右岸R,荷叶Y,石柱S,3# 青蛙从 Y R;1# 青蛙从 S Y;2# 青蛙从 S R;1# 青蛙从 Y R;结论:Jump(
20、1,1)=4 2*Jump(0,1)=2*2,参照汉诺塔问题将借助一个石柱( s=1 )、一个荷叶( y=1 )的青蛙跳跃问题分成五个步骤:第一步:借助荷叶将左岸上面的若干青蛙(此时是1#与2#两只)跳到石柱上暂存;第二步:左岸下一只青蛙(此时是3#)跳到荷叶上;第三步:左岸再下一只青蛙(此时是4#)跳到右岸;第四步:荷叶暂存的青蛙( 3# )跳到到右岸;第五步:石柱上暂存青蛙(1#、2#)借助荷叶完成到跳到右岸。,以上的石柱如果看成右岸(步骤1)或左岸(步骤2)的话,进一步的分析,还可以将上述五个步骤分成以下两个阶段:第一、五两步实际上完成的就是青蛙借助一个荷叶跳跃的过程,并且这两步的对象是
21、同一批青蛙,青蛙个数是Jump(0,1)。第二、三、四步实际上完成的也是青蛙借助一个荷叶跳跃的过程,青蛙个数是Jump(0,1)。所以:Jump(1,1)的值是Jump(0,1)+ Jump(0,1) 即:Jump(1,1)=2*Jump(0,1);,对于借助s个石柱、y个荷叶的青蛙跳跃过程也可以类似的归纳出来,这个实现步骤可以分成七个步骤:第一步:借助所有荷叶以及其余石柱将左岸上面的若干青蛙跳到第一个石柱上暂存;第二步:左岸余下的青蛙借助其它可用的石柱以及所有荷叶跳到其它石柱上;第三步:左岸再余下的青蛙跳到所有荷叶上;第四步:左岸再下一只青蛙完成到右岸的直接跳跃-最大1只第五步:荷叶暂存的青
22、蛙完成到右岸的跳跃;第六步:除了第一个外其余石柱上暂存青蛙完成到右岸的跳跃;第七步:第一个石柱上暂存的青蛙借助其余石柱以及所有荷叶完成到右岸的跳跃;,如果以上的石柱在跳跃过程中可以看成右岸或左岸,这七个步骤也可以简化成两个阶段:第一、七两步实际上是青蛙借助s-1个石柱以及y个荷叶,完成从左岸到第一个石柱,再到右岸的跳跃的过程,而这两步的对象是同一批青蛙,青蛙个数是Jump(s-1,y)。第二步到第六步完成的是左岸的青蛙借助剩余的n-1个石柱以及所有y个荷叶跳跃到右岸的过程,青蛙个数是Jump(s-1,y)。结论:Jump(s, y)是Jump(s-1,y)+ Jump(s-1,y)。 即:Ju
23、mp(s,y)=2*Jump(s-1,y);注意:当石柱数目为0是不需要递归的,此时跳跃的青蛙数目为荷叶数目+1。即递归的结束条件为:Jump(0,y)=y+1;,L,R,Y,S2,S1,例如:荷叶数是1、石柱数是2的Jump(2,1)=2* Jump(1,1)= 8,int Jump(int s,int y) /青蛙过河递归函数 int k;if (s=0) / s=0表示无石柱 , 则为直接可解结点 k=y+1;else / 如果 r 不为0 , 则要 Jump(r-1,z) k=2*Jump(s-1,y);return(k);,#include void main( ) int s,y,
24、sum; / s 为河中石柱数 , y为荷叶数printf(请输入石柱数s= ); scanf(%d, ,汉诺塔游戏与青蛙跳河的不同,1)初始位置、结束位置不同;2)在汉诺塔游戏中,所搬盘子总数n的值决定第一个盘子是搬到B、还是搬到C;3)在青蛙跳河中,青蛙总数n的值不决定第一个青蛙搬到那个石柱上。,选择排序、冒泡排序的排序思路比较简单,但是排序效率较低,不能满足需求(比如在OJ或比赛题目中)。效率更高的排序方法呢?快速排序是利用分治递归技术实现的一种高效的方法。分治法简而言之就是“分而治之”,即把一个复杂的问题分成两个或更多的子问题,再把子问题分成更小的子问题直到最后分解成的子问题可以直接求
25、解,原问题的解即子问题的解的合并。,递归设计实例3-快速排序,将要求解的较大规模的问题分割成k个更小规模的子问题。,分治法思想,n,T(n/2),T(n/2),T(n/2),T(n/2),T(n),=,对子问题分别求解。如果子问题的规模仍然不够小,则再划分子问题,如此递归的进行下去,直到问题规模足够小,很容易求出其解为止。,分治法思想,n,T(n),=,将求出的小规模的问题的解合并为一个更大规模的问题的解,自底向上逐步求出原来问题的解。,快速排序的思想,快速排序是基于分治递归的思想来实现的:1)设要排序的数组是a0an-1;2)任意选取一个数据(通常选用第一个数据)作为关键数据;3)将所有比关
26、键数据小的数都放到它前面(左),所有比关键数据大的数都放到它后面(右)-这个过程称为一趟快速排序。4)对关键数据前、后的数据再分别进行一趟快速排序,直到参加排序的数字是一个为止。,一趟快速排序的算法步骤如下: 1)设置两个变量i、j,排序初:i=0,j=n-1; 2)以 a0 作为关键数据,即 key=a0; 3)从 j 开始向前搜索,即由后开始向前搜索(j=j-1),找到第一个小于key的值aj,将其值赋给ai; 4)从i开始向后搜索,即由前开始向后搜索(i=i+1),找到第一个大于key的ai,将其值赋给aj; 5)重复第3)、4)步,直到 i=j,此时将key暂存的值赋给ai(或aj);
27、 注意:一趟快速排序完成,其结果是以key作为轴心数据,把数组的原数据分为 2 部分。比 key 小的数据放在key的前面(左),比 key 大的数据放在 key 的后面(右)。,key,54,52,53,51,5=key,j往前移动(j-),继续比较下一个aj,直到遇到小于key的元素停止。将此时的小于key的aj赋值给前面的元素ai,即: ai=aj,同时i后移(i+),指向下一个元素。,78,25,一趟快速排序的算法举例2(从小到大):,50,45,80,36,15,60,72,92,25,78,i,j,a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,key=50,过程2:
28、从前面的元素(i所指)与key进行比较,如果ai=key,i往后移动(i+),继续比较下一个ai,直到遇到大于key的元素停止。 将此时的大于key的ai赋值给前面的元素aj,即:aj=ai,同时j前移(j-),指向下一个元素。,78,25,45,80,一趟快速排序的算法举例2(从小到大):,50,45,80,36,15,60,72,92,25,78,a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,key=50,重复上述过程1与过程2两个步骤,直到下标 i 与下标 j 指向同一元素(即i=j)为止。当 i=j 时,i(或j)所指的位置就是初始选作关键数据key应该存放的位置,把key放入该位置。 . 一趟快速排序的过程,