第 5 章递推法 5.算法设计思想 1 递推法是利用所求解问题本身具有的性质(递推关系)来求问题解的 有效方法。 n 之前的一步(1) n-n 具体做法是根据该问题 N =n-或多步(1,2, n-3,…) 的结果推导出 n 时的解:f(=F(n-1),n-2),…)( n)f(f(称 为递推关系式); 而 N 0,…的初值f(f(…往往是直接给出或直 观得出的。 =1,0),1), 递推算法的关键问题是得到相邻的数据项之间的关系,即递推关系。 递推关系是一种高效的数学模型,是递推应用的核心。递推关系不仅在各 数学分支中发挥着重要的作用,而且由它体现出来的递推思想在各学科领 域中更是显示了独特的魅力。 在求解具体问题时, N = n 的解到底与其前面的几步结果相关必须明 确。假设是两步,那么在计算的过程中只需记住这前两步的结果R1、R2, 下一步的结果 R 可以由这前两步的结果推导得到,即有R=F(R1,R2), 接下来进行递推:R1=R2,R2=R,向后传递,为求下一步的结果做好准 备。这也正是递推法名字的由来。若问题与前三步相关,则在计算的过程 中需要记住前三步的结果R1、R2、R3,下一步的结果 R 可由这前三步的结 果推导得到,即有R=F(R1,R2,R3), 接下来进行递推:R1=R2,R2= R3,R3=R,向后传递。 递推法的一般步骤如下。 数组 ( 。 1)确定递推变量。递推变量可以是简单变量,也可以是一维或多维 (2)建立递推关系。递推关系是递推的依据,是解决递推问题的关键。 (3)确定初始(边界)条件。根据问题最简单情形的数据确定递推变量 的初始(边界)值,这是递推的基础。 (4)控制递推过程。递推过程控制通常由循环结构实现,即递推在什 么时候进行,满足什么条件结束。 72 算法设计方法与优化(第2 版) 递推法可分为正推法和倒推法两种。其中,正推法是一种简单的递推方式,是从小规 模的问题推解出大规模问题的一种方法,也称为“正推”;倒推法则是对某些特殊问题所采 用的不同于通常习惯的一种方法,即从后向前进行推导,实现求解问题的方法。 5.2 典型例题 5.2.1 兔子繁殖问题 1.问题描述 一对兔子从出生后第三个月开始,每月生一对小兔子。小兔子长到第三个月又开始 生下一代小兔子。假若兔子只生不死,一月份抱来一对刚出生的小兔子,问一年中的每个 月各有多少对兔子。 2.问题分析 寻找问题的规律性,需要通过对现实问题的具体事例进行分析,从而抽象出其中的 规律。一 对兔子从出生后第三个月开始每月生一对小兔子,第三个月以后每月除上个月的 兔子外,还有新生的小兔子,在下面用加号后面的数字表示。则第三个月以后兔子的对数 就是前两个月兔子对数的和。过程如下: 1月2月3月4月5月6月… 1 1 1+1=2 2+1=3 3+2=5 5+3=8 … 3.算法说明 算法说明参见表5-1。 表 5-1 类 型名 称代表的含义 算法rabbit() 递推法求解兔子繁殖问题 变量a 当前月的前两个月的兔子对数 变量b 当前月的前一个月的兔子对数 变量c 当前月份的兔子对数 4.算法设计 #include "stdio.h" void rabbit() { int i,c,a=1,b=1; printf("%d,%d,",a,b); for(i=1;i<=10;i++) { 第5 章 递推法 73 c=a+b; /*斐波那契数列规律,第三项等于前两项之和*/ printf("%d,",c); a=b; /*向后递推*/ b=c; } }v oid main() { printf("一年中每个月兔子对数如下:\n"); rabbit(); } 5.运行结果 6.算法优化 1)优化说明 1 2 3 4 5 6 7 8 … a b a=a+b b=b+a a=a+b b=b+a a=a+b b=b+a … 因此,可以归纳出用“a=a+b,b=b+a”做循环不变式,这样一来,循环其实是递推 了2步,循环次数自然减少。 2)算法说明 算法说明参见表5-1。 3)算法设计 #include "stdio.h" void rabbit() { int i,a=1,b=1; printf("%d,%d,",a,b); for(i=1;i<=5;i++) { a=a+b; b=b+a; printf("%d,%d,",a,b); } }v oid main() { rabbit(); } 74 算法设计方法与优化(第2 版) 5.2.2 最大公约数问题 1.问题描述 给出任意两个正整数,求它们的最大公约数。 2.问题分析 求两个数的最大公约数,最简单的方法就是用最大公约数的定义来求。 设m ,n 为两个正整数,d 取m 和n 中较小的一个,若d≠0,则判断d 是否同时整除 m ,n,如果是,则d 为最大公约数;否则,d=d-1重复上述过程,直到满足为止。因为最 后d=1时,一定会满足条件。 3.算法说明 算法说明参见表5-2。 表 5-2 类 型名 称代表的含义 算法f(intm,intn) 由定义求最大公约数 形参变量m,n 输入的两个数 变量d 保存最大公约数 4.算法设计 #include "stdio.h" int f(int m,int n) { int d; if(m<n) d=m; else d=n; while(d>0) { if(m%d==0 && n%d==0) return(d); d--; } }v oid main() { int m,n; printf("请输入两个正整数:"); scanf("%d%d",&m,&n); printf("最大公约数为:%d\n",f(m,n)); } 第5 章 递推法 75 5.运行结果 6.算法优化 1)优化说明 下面应用递推法求解此问题。 在数学中,求最大公约数有一个很有名的方法叫辗转相除法,辗转相除法体现了递推 法的基本思想。 设m ,n 为两个正整数,且n 不为零,辗转相除法的过程如下。 (1)将问题转化为数学公式,即r=m %n,r 为m 除以n 的余数。 (2)若r=0,则n 即为所求的最大公约数,输出n。 (3)若r!=0,则令m =n,n=r,继续递归,再重复前面的(1)和(2)步骤。 其中第(3)步即为递推部分。 2)算法说明 算法说明参见表5-3。 表 5-3 类 型名 称代表的含义 算法f(intm,intn) 辗转相除法求最大公约数 形参变量m,n 输入的两个数 变量r 两个数相除的余数 3)算法设计 #include "stdio.h" int f(int m,int n) { int r=n; while(r!=0) { r=m%n; /*利用r=m%n,将余数赋给r*/ m=n; n=r; } return m; }v oid main() { int m,n; printf("Input two numbers:"); scanf("%d%d",&m,&n); 76 算法设计方法与优化(第2 版) printf("%d\n",f(m,n)); } 5.2.3 猴子吃桃问题 1.问题描述 一只小猴子摘了若干个桃子,每天吃现有桃子的一半多一个,到第10天时只剩一个 桃子,求小猴子最初摘了多少个桃子? 2.问题分析 这道题可以用倒推法来解决。因为猴子每天吃的桃子数取决于前一天的桃子数,所 以可以用一个递推变量代替桃子数。设a 为递推变量,代表今天剩下的桃子数,那么就 可以推导出昨天剩下的桃子数,即a=(a+1)×2,这就是本题的递推关系式,找到这个关 系式,问题基本就解决了。 3.算法说明 算法说明参见表5-4。 表 5-4 类 型名 称代表的含义 算法tao() 递推法求解猴子吃桃问题 变量a 桃子数 变量i 天数 4.算法设计 #include "stdio.h" void tao() { int i,a; a=1; for(i=9;i>=1;i--) /*利用倒推法求解*/ a=(a+1)*2; printf("sum=%d\n",a); }v oid main() { tao(); } 5.运行结果 第5 章 递推法 77 6.算法优化 1)优化说明 这道题不但可以用倒推法解决,还可以用递归法求解。 由于猴子每天吃的桃子数取决于前一天的桃子数,因此可定义一个递归函数来求桃 子数,设tao(n)代表第n 天剩下的桃子数,则有递归关系式: tao(n)=(tao(n +1)+1)×2 当n =1,2,3,…,9时 递归终止条件是:当n=10时,tao(n)=1。 2)算法说明 算法说明参见表5-5。 表 5-5 类 型名 称代表的含义 算法tao(intn) 求解猴子吃桃问题算法 形参变量n 天数 3)算法设计 #include "stdio.h" int tao(int n) { if (n==10) return 1; else return (tao(n+1)+1)*2; }v oid main() { printf("sum=%d\n",tao(1)); } 5.2.4 杨辉三角形问题 1.问题描述 输出如图5-1所示的杨辉三角形(打印出10行)。 图 5-1 2.问题分析 由图5-1所示的杨辉三角形可以看出,中间的数据等于其 上一行左上、右上的数据和,第i 层有i 列需要求解i 个数据。 可以用二维数组array[][]存储杨辉三角形。 为了便于表示,可以将杨辉三角形变一下形状,即从第一 列开始放置,则可得到一个下三角矩阵,而且很有规律:第一 列都为1,主对角线都为1,从第三行起,中间(除第一个和对角 线之外)位置元素的值等于其上一行对应位置元素及其前一个元素之和,这就是从当前行 78 算法设计方法与优化(第2 版) 推导到下一行的递推关系式,由此便可求出杨辉三角形的任一行。 3.算法说明 算法说明参见表5-6。 表 5-6 类 型名 称代表的含义 算法yanghui() 递推法求解杨辉三角形问题 二维数组array 存储杨辉三角形 变量i 行 变量j 列 4.算法设计 #include "stdio.h" void yanghui() { int array[10][10],i,j; for(i=0;i<10;i++) { array[i][i]=array[i][0]=1; for(j=1;j<=i;j++) array[i+1][j]=array[i][j]+array[i][j-1]; } for(i=0;i<10;i++) { for(j=0;j<=i;j++) { printf("%5d",array[i][j]); printf(" "); } printf("\n"); } }v oid main() { yanghui(); } 5.运行结果 第5 章 递推法 79 6.算法优化 1)优化说明 前面以二维数组为存储,使用递推法求解杨辉三角形问题,那么,是否可以用一维数 组为存储来解决本问题呢? 回答当然是肯定的。 使用一维数组时注意以下两个问题:一是不能保存完整的杨辉三角形,只能求出一 行输出一行;二是在利用递推法从上一行推导下一行时,如果按照通常从前向后推导的 话,则原有已知数据将被覆盖,怎么办? 只好倒过来从后向前逐项计算,这也是一种倒 推法。下 面举例说明。设有一维数组a[],现已存储第三行数据,则有a[1]=1,a[2]=2, a[3]=1,那么,下一行的求值顺序是:先求a[4],已知所有数组元素初值均为0,因此, a[4]=a[3]+a[4]=1+0=1;再求a[3]=a[2]+a[3]=2+1=3;以此类推。 2)算法说明 算法说明参见表5-7。 表 5-7 类 型名 称代表的含义 算法yanghui() 递推法求解杨辉三角形问题 一维数组a 存储杨辉三角形 变量i 杨辉三角形的行数 变量j 数组下标 3)算法设计 #include "stdio.h" void yanghui() { int n,i,j,a[20]; printf(" 1\n"); a[1]=a[2]=1; printf(" %-4d %-4d\n",a[1],a[2]); for(i=3;i<=10;i++) { a[1]=a[i]=1; for(j=i-1;j>1;j--) a[j]=a[j]+a[j-1]; for(j=1;j<=i;j++) printf(" %-4d ",a[j]); printf("\n"); } }v oid main() { yanghui(); } 80 算法设计方法与优化(第2 版) 5.2.5 伯努利装错信封问题 1.问题描述 某人写了n 封信,这n 封信对应有n 个信封。求把所有的信都装错了信封的情况共 有多少种? 这是组合数学中有名的错位问题。著名数学家伯努利(Bernoulli)曾最先考虑此题。 后来,欧拉对此题产生了兴趣,称此题是“组合理论的一个妙题”,独立地解出了此题。 2.问题分析 为叙述方便,把某一元素在自己相应位置(如“2”在第2个位置)称为在自然位;某一 元素不在自己相应位置称为错位。 事实上,所有全排列分为以下三类。 (1)所有元素都在自然位,实际上只有一个排列。当n=5时,即12345。 (2)所有元素都错位。当n=5时,如24513。 (3)部分元素在自然位,部分元素错位。当n=5时,如21354。 装错信封问题求解实际上是求n 个元素全排列中的“每一元素都错位”的子集。 当n=2时显然只有一个解:21(“2”不在第2个位置且“1”不在第1个位置)。 当n=3时,有231,312两个解。 通常,可在实现排列过程中加上“限制取位”的条件。 设置一维数组a[]。a[i]在1~n 中取值,出现数字相同a[j]=a[i]或元素在自然 位j=a[j]时返回(j=1,2,…,n-1)。 当i<n 时,还未取n 个数,i 增1后a[i]=1继续; 当i=n 且最后一个元素不在自然位a[n]!=n 时,输出一个错位排列,并设置变量s 统计错位排列的个数。 当a[i]<n 时,a[i]增1继续。 当a[i]=n 时,回溯或调整,直到i=1且a[1]=n 时结束。 3.算法说明 算法说明参见表5-8。 表 5-8 类 型名 称代表的含义 算法bernoulli(intn) 回溯法求解伯努利装错信封问题 形参变量n 信封个数 一维数组a 标记信和信封是否错位 变量s 统计全错位总和 4.算法设计 #include "stdio.h" int bernoulli(int n)