【从算法小白到 csp-j 一等 第三节】递推与递归
【从算法小白到 csp-j 一等 第三节】递推与递归
- 前言
- 1 递推
- 1.1 递推的定义
- 1.2 递推的应用
- 2 递归
- 2.1 递归的定义
- 2.2 递归的优化
- 3 例题
- 3.1 [NOIP2003 普及组] 栈
- 题目背景
- 题目描述
- 输入格式
- 输出格式
- 样例 #1
- 样例输入 #1
- 样例输出 #1
- 提示
- 做法
- 3.2 台阶问题
- 题目描述
- 输入格式
- 输出格式
- 样例 #1
- 样例输入 #1
- 样例输出 #1
- 提示
- 做法
- 4 习题
- 结语
前言
递归与地推在算法竞赛中的重要性及其重要,是之后动态规划与搜索的重要基础。
1 递推
1.1 递推的定义
递推指从给定条件出发,依据某种递推关系,依次推出所有中间结果及目标结果的方法。
1.2 递推的应用
以 F i b o n a c c i Fibonacci Fibonacci 数列为例,设 F F F 为 F i b o n a c c i Fibonacci Fibonacci 数列,则 F 0 = F 1 = 1 F_0 = F_1 = 1 F0=F1=1, F i = F i − 1 + F i − 2 F_i = F_{i - 1} + F_{i - 2} Fi=Fi−1+Fi−2,其中 F i = F i − 1 + F i − 2 F_i = F_{i - 1} + F_{i - 2} Fi=Fi−1+Fi−2 就是递推关系。
可以开一个一维数组 F F F 表示 F i b o n a c c i Fibonacci Fibonacci 数列,将初始值 F 0 F_0 F0 和 F 1 F_1 F1 设置为 1 1 1,后面从二开始一一递推即可。
const int N = 100010; // 数组大小,可以自己设置
int F[N] = {1, 1} // 设置初始值
int main()
{
int n; // 需要计算的项数
scanf("%d", &n);
for (int i = 2; i <= n; i ++ ) // 从 2 到 n 递推
F[i] = F[i - 1] + F[i - 2];
printf("%d\n", F[N]);
return 0;
}
当然,这个问题也可以用递归解决。
2 递归
2.1 递归的定义
递归指把一个大型复杂的问题层层转化为一个与原问题相似的、规模较小的问题来求解的方法。
递归有两个要求:
- 化成的子问题和原问题求解方法相同,只是数字大小不同。
- 递归次数有限,即递归到一定程度,递归函数可以结束,有结束的条件,即递归的边界。
F i b o n a c c i Fibonacci Fibonacci 数列同样也可以同递归解决,实现递归函数,函数调用自己实现转化为小问题解决。
int F(int x) // 递归函数
{
if (x == 0 || x == 1) return 1; // 递归出口
return F(x - 1) + F(x - 2); // 转化成子问题求解。
}
int main()
{
int n;
scanf("%d", &n);
printf("%d\n", F(n));
}
递归的优势在于可以得到转化到每一个子问题的时间。如果在转化到每一个子问题时有输出,递归无疑是更好的选择。
递归还能在处理递推关系时更加方便。在 F i b o n a c c i Fibonacci Fibonacci 数列中,递推是直接从 2 2 2 到 n n n 枚举即可,但当问题的每个中间部分的求解顺序较为复杂时,这样做并不简单,而递归却没有这类问题。
递归还可以用于枚举,比如枚举长度固定,单调递减的序列。递归只需记录上一个数,即可确定当前数的范围。
2.2 递归的优化
但经过观察,我们发现,上述代码中,在 n n n 很大时( n = 100000 n = 100000 n=100000),递归比递推慢很多,很长时间无法出解。原因是递归可能重复调用相同参数的递归函数,导致重复计算浪费时间。可以在每一个子问题计算完后,标记答案,下次再调用直接用之间计算的答案即可(前提是子问题结果只和函数参数有关)。
const int N = 100010;
int f[N]; // 储存之前的答案
int F(int x)
{
if (f[x] >= 0) return f[x]; // 如果之前计算过不重复计算
if (x == 0 || x == 1) f[x] = 1;
else f[x] = F(x - 1) + F(x - 2); // 递归求解
return f[x];
}
int main()
{
int n;
scanf("%d", &n);
memset(f, -1, sizeof f); // 初始化成没有计算过
printf("%d\n", &F(n));
}
这样递归就得到了优化。
3 例题
3.1 [NOIP2003 普及组] 栈
例题一
题目背景
栈是计算机中经典的数据结构,简单的说,栈就是限制在一端进行插入删除操作的线性表。
栈有两种最重要的操作,即 pop(从栈顶弹出一个元素)和 push(将一个元素进栈)。
栈的重要性不言自明,任何一门数据结构的课程都会介绍栈。宁宁同学在复习栈的基本概念时,想到了一个书上没有讲过的问题,而他自己无法给出答案,所以需要你的帮忙。
题目描述
宁宁考虑的是这样一个问题:一个操作数序列, 1 , 2 , … , n 1,2,\ldots ,n 1,2,…,n(图示为 1 到 3 的情况),栈 A 的深度大于 n n n。
现在可以进行两种操作,
- 将一个数,从操作数序列的头端移到栈的头端(对应数据结构栈的 push 操作)
- 将一个数,从栈的头端移到输出序列的尾端(对应数据结构栈的 pop 操作)
使用这两种操作,由一个操作数序列就可以得到一系列的输出序列,下图所示为由 1 2 3
生成序列 2 3 1
的过程。
(原始状态如上图所示)
你的程序将对给定的 n n n,计算并输出由操作数序列 1 , 2 , … , n 1,2,\ldots,n 1,2,…,n 经过操作可能得到的输出序列的总数。
输入格式
输入文件只含一个整数 n n n( 1 ≤ n ≤ 18 1 \leq n \leq 18 1≤n≤18)。
输出格式
输出文件只有一行,即可能输出序列的总数目。
样例 #1
样例输入 #1
3
样例输出 #1
5
提示
【题目来源】
做法
NOIP 2003 普及组第三题
本题可以用递归枚举所有方案数。
在函数外维护一个栈,表示当前栈情况。递归函数中传参数为当前从初始情况序列取了多少个数。递归时分两种情况,处理参数和栈的变化。最终如果栈和原序列都取空了,把序列加到可能序列当中。最终输出数量即可。
本做法效率较低(因为只能应用当前知识点)。更快速的算法参照洛谷题解。
3.2 台阶问题
例题二
题目描述
有 N N N 级台阶,你一开始在底部,每次可以向上迈 1 ∼ K 1\sim K 1∼K 级台阶,问到达第 N N N 级台阶有多少种不同方式。
输入格式
两个正整数 N , K N,K N,K。
输出格式
一个正整数 a n s ( m o d 100003 ) ans\pmod{100003} ans(mod100003),为到达第 N N N 级台阶的不同方式数。
样例 #1
样例输入 #1
5 2
样例输出 #1
8
提示
- 对于 20 % 20\% 20% 的数据, 1 ≤ N ≤ 10 1\leq N\leq10 1≤N≤10, 1 ≤ K ≤ 3 1\leq K\leq3 1≤K≤3;
- 对于 40 % 40\% 40% 的数据, 1 ≤ N ≤ 1000 1\leq N\leq1000 1≤N≤1000;
- 对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 100000 1\leq N\leq100000 1≤N≤100000, 1 ≤ K ≤ 100 1\leq K\leq100 1≤K≤100。
做法
本题没有明显的递推式,需要我们自己推导。
设 f i f_i fi 为还剩 i i i 级台阶的方案数, f 0 = 1 f_0 = 1 f0=1, f n f_n fn 即为答案。
对于每个 f i f_i fi 考虑。如果迈 j j j 级台阶,之后的方案数 f i − j f_{i - j} fi−j。由于决策不同,方案数应该加起来,即 f i = ∑ j = 1 m i n ( i , k ) f i − j f_i = \sum_{j = 1}^{min(i, k)} f_{i - j} fi=∑j=1min(i,k)fi−j。
时间复杂度 O ( n k ) O(nk) O(nk),可以通过此题。
4 习题
禽兽的传染病
月落乌啼算钱(斐波那契数列)
[NOIP2001 普及组] 数的计算
迷宫
其中一、二题较简单,三、四题较难,建议先独立思考,不会再看题解。
结语
递推与递归是重要基础,是后面搜索和动态规划基础,搜索和动态规划又是整个算法竞赛的两个重要部分。只有打下坚实的基础,才能在算法竞赛的道路上走得更远。