第一次课实验题题解
第一节课的前三道实验题,题目要求“使用递归”,但递归好像对很多同学来说都比较陌生,再加上题目本身(尤其是第三题)真的很难,所以这篇文章将先大体讲一下递归是什么,然后以“从递推到递归”的思路完成前两道题,并给出递推版本题解和符合要求的递归版本题解。等大家对递归熟悉后,再直接运用递归解决最难的第三题。
递归
总而言之,递归就是函数自己调用自己。递归是一个很难理解的算法,我们先看下面这个引例来了解一下,对递归已经很熟悉的同学可以跳过这部分。
递归的引例
int num=0; //计数器,用于记录递归调用次数
int find(int n){
if(n==1)return num; //递归结束,返回结果
n=n-1; //n减小
num++; //num++表示运行了一次
return find(n); //递归调用
}
}
- 如果
n=1
,函数在第一个if
直接结束,此时num=0
直接被返回; - 如果
n=2
,函数不会在第一个if
时终止,而是执行它下面的语句,最后返回find(1)
,然后程序再掉过头来找find(1)
是什么,这时候n经过一次n=n-1
后,已经为1了,所以在第一个if
里终止,而此时num
也经历过一次增大,所以返回的num
将是1
现在要求程序执行find(n)
,其中n=4
,我们来手推这段程序:
-
n=4
因此不会触发return num
,而是执行下面的语句,n=n-1=3
,num=1
,下一句话是return find(3)
,但目前还不知道find(3)
是什么,所以这个函数运行到这里先暂停,去找find(3)
到底是什么 -
n=3
因此不会触发return num
,而是执行下面的语句,n=n-1=2
,num=2
,下一句话是return find(2)
,但目前还不知道find(2)
是什么,所以这个函数运行到这里先暂停,去找find(2)
到底是什么 -
n=2
因此不会触发return num
,而是执行下面的语句,n=n-1=1
,num=3
,下一句话是return find(1)
,但目前还不知道find(1)
是什么,所以这个函数运行到这里先暂停,去找find(1)
到底是什么 -
n=1
触发return num
,此时的num=3
,也就是说经历多次递归调用后,我们找到了这时候的find(1)=3
,那么函数find(2)
可以继续了,返回3;那么函数find(3)
可以继续了,返回3;那么函数find(4)
可以继续了,返回3;
综上,如果在主函数中这样写:
int main()
{
int n=4;
cout<<find(n);
return 0;
}
输出:
3
递归大概就是这样的一个过程:函数执行的时候,遇到了“参数不同的它自己”,然后它先放下手里的其他工作,去探索这个新的“它自己”是什么,在探索过程中又遇到了一个“更新的它自己”,于是现在的工作暂停,出发去探索新的“它自己”是什么······等最后终于没有”新的自己“出现了,再把探索的结果一层一层地往回带,最后回到原来想要问题,得到结果。
从上面的过程中,我们可以总结出两个递推的普遍特点:
- 需要有一个递推终点,“新的自己”不应该是无限多的,这样程序才有停下来的可能,比如上面程序中的
if(n==1)return num;
- 需要通过操作来靠近递推终点,比如上面程序中的
n--;
递归的确不是很好懂,希望你看完上面的过程后,对它能有一个初步的理解,再通过后续的练习加深理解。
btw. 有句老话说的好,人理解递推,神理解递归
第一题——递归求和
题目链接:传送门
描述
递归是一种非常有效的程序设计方法,应用相当广泛,递归求和就是其中的一种。现在定义数列通项An = n * n,给定一个整数n(1 <= n <= 1000),要你求前n项和Sn,即Sn = 1 * 1 + 2 * 2 + … + n * n。要求使用递归的方法进行计算。
输入
输入只有一行,包括一个整数n,表示要求的项数。
输出
输出只有一行,为一个整数Sn,表示对应的前n项和。
样例输入
7
样例输出
140
解题思路
题意很清晰,数列An=n*n
, 它还有个和数列Sn=A1+A2+...+An
,我们的任务就是对一个给定的n
,求Sn
递推解
因为我们对递归还不够熟悉,不妨先用递推写一下试试,可以得到递推关系:S(n)=S(n-1)+n*n
#include <iostream>
using namespace std;
int s[1010]; //和数列S
int main()
{
s[1]=1;
int n;
cin>>n;
for(int i=2;i<=n;i++)
s[i]=s[i-1]+i*i; //递推关系
cout<<s[n];
return 0;
}
提交结果是Accepted
递归解
我们直接把递推公式以递归的形式表述出来(暂时不考虑递推终点和操作)
int s(int n){
return s(n-1)+n*n;
}
代入递推公式后,我们发现它已经满足了第二个条件——n在不断减小,而n越小,题目越简单,当n=1
的时候,我们知道结果是1,可以把它设为递推终点
int s(int n){
if(n==1)return 1; //如果当前n=1,递归结束,返回1
return s(n-1)+n*n; //否则,继续递归
}
上面这个递归函数,就能完美满足题目要求
附一下完整程序,提交结果Accepted
#include <iostream>
using namespace std;
int s(int n){
if(n==1)return 1;
return s(n-1)+n*n;
}
int main()
{
int n;
cin>>n;
cout<<s(n);
return 0;
}
第二题——递归求最大公约数
题目链接:传送门
描述
给定两个正整数,求它们的最大公约数
输入
输入一行,包含两个正整数,两数都在32位有符号整型的表示范围之内。(也就是在int范围内)
输出
输出一个正整数,即这两个正整数的最大公约数
样例输入
6 9
样例输出
3
提示
整数x和y的最大公约数是能够同时整除x和y的最大整数。编写递归函数gcd来返回x和y的最大公约数。x和y的gcd函数按如下方式进行递归定义:如果y等于0,那么gcd(x, y)等于x;否则gcd(x,y)等于gcd(y, x % y),这里%是求模运算符。
解体思路
老师给的提示,运用到的数学方法称为“辗转相除法”,是一种求两个数最大公约数的套路化方法,有兴趣了解其证明的同学可以自行百度,这里将直接对其进行运用,按照从递推到递归的思路给出题解
ps. gcd
只是一个自己起的名字,你想把它改成任何名字都可以,只不过实际应用中为了增强代码的可读性,往往都把“用来求最大公因数的函数”取名为gcd
递推解
使用辗转相除法和while(r!=0)
进行递推,当r=0
时while
结束,输出b
,详见下方代码:
#include <iostream>
using namespace std;
int main()
{
int a, b; //待输入的两个数
cin>>a>>b;
int r=a%b; //定义r为a/b的余数
while(r!=0){ //r为0时的b即为最大公约数
a=b;
b=r;
r=a%b;
}
cout<<b;
return 0;
}
有关辗转相除法,建议自己随便写两个数,然后手推一下,验证它的可行性,同时也加深记忆。这里给出一个手推例子:24和16
a=24,b=16,r=24%16=8,c!=0;
a=b=16,b=r=8,r=16%8=0,c==0;
r=0时,b=8即为24和16的最大公因数
提交结果Accepted
递归解
#include <iostream>
using namespace std;
int gcd(int x, int y){
int r=x%y; //余数r
if(r==0)return y; //递归终点
x=y;
y=r;
return gcd(x, y); //执行新的递归
}
int main()
{
int a,b;
cin>>a>>b;
cout<<gcd(a,b);
return 0;
}
第三题——递归:爬楼梯
题目链接:传送门
描述
小明爬楼梯,他可以每次走1级或者2级,输入楼梯的级数,求不同的走法数。
例如:楼梯一共有3级,他可以每次都走一级,或者第一次走一级,第二次走两级;也可以第一次走两级,第二次走一级,一共3种方法。
输入
输入包含若干行正整数,第一行正整数K代表数据组数;后面K行,每行包含一个正整数N,代表楼梯级数,1 <= N <= 30
输出
不同的走法数,每一行输入对应一行输出
样例输入
3
5
8
9
样例输出
8
34
89
解题思路
这个题真的是宇宙无敌超级好题,我想详细讲一下这个题的递推公式是怎么来的,再顺便说一下
“输入包含若干行正整数,第一行正整数K代表数据组数;后面K行,每行包含…”
这种题目要求,应该如何处理
递推公式
我们现在考虑一般情况,用f(n)
表示到第n阶的走法种类数,且我们假设已经算出来f(n-1)
和它之前的所有项,那么问自己这样一个问题:
想要到达第n阶,有几种可能?
- 从第(n-1)阶出发,迈一步过来
- 从第(n-2)阶出发,迈两步过来
想要到第n阶,只有这两种可能性
等等,我从第n-2阶出发,先迈一步,再迈一步,不也是一种可能吗?
不是,因为你先迈一步,到了n-1阶以后,问题变成了“从第n-1阶到第n阶”,这种可能性已经被算入第一种情况
综上,到达第n阶的f(n)
种走法被分成了两部分:
- 来自第n-1阶的一部分,有
f(n-1)
种 - 来自第n-2阶的另一部分,有
f(n-2)
种
也就是说,我们得到了递推公式:
f(n)=f(n-1)+f(n-2);
做到这,你已经可以用递推写出答案来了,但我们能不能尝试一下不经过递推,直接写递归?
我们之前给过一个假设,那就是“已经算出来f(n-1)
和它之前的所有项”,但事实上我们根本不知道它们是多少,而是需要现算:
如果遇到f(n-1)
,我们需要用f(n-2)+f(n-3)
去算它,而f(n-2)
和f(n-3)
也不知道,也需要用这样的规律继续去算,直到不依赖规律,我们也能得到结果,比如算到f(3)=f(2)+f(1)
,我们知道f(1)=1
,f(2)=2
。
这其实跟计算机的递归调用过程是一样的。
int f(int n){
if(n==1)return 1; //递归终点
if(n==2)return 2; //递归终点
return f(n-1)+f(n-2); //递归调用
}
n组输入&输出的处理
这个东西就像一个“套路”一样,只要记住就可以,并且也不难理解
int t; //t组数据
cin>>t;
while(t--){
...
}
题解代码
#include <iostream>
using namespace std;
int f(int n){
if(n==1)return 1;
if(n==2)return 2;
return f(n-1)+f(n-2);
}
int main()
{
int t,n;
cin>>t;
while(t--){
cin>>n;
cout<<f(n)<<endl;
}
return 0;
}