当前位置: 首页 > article >正文

【从算法小白到 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=Fi1+Fi2,其中 F i = F i − 1 + F i − 2 F_i = F_{i - 1} + F_{i - 2} Fi=Fi1+Fi2 就是递推关系。

可以开一个一维数组 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 递归的定义

递归指把一个大型复杂的问题层层转化为一个与原问题相似的、规模较小的问题来求解的方法。

递归有两个要求:

  1. 化成的子问题和原问题求解方法相同,只是数字大小不同。
  2. 递归次数有限,即递归到一定程度,递归函数可以结束,有结束的条件,即递归的边界。

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

现在可以进行两种操作,

  1. 将一个数,从操作数序列的头端移到栈的头端(对应数据结构栈的 push 操作)
  2. 将一个数,从栈的头端移到输出序列的尾端(对应数据结构栈的 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 1n18)。

输出格式

输出文件只有一行,即可能输出序列的总数目。

样例 #1

样例输入 #1
3
样例输出 #1
5

提示

【题目来源】

做法

NOIP 2003 普及组第三题

本题可以用递归枚举所有方案数。

在函数外维护一个栈,表示当前栈情况。递归函数中传参数为当前从初始情况序列取了多少个数。递归时分两种情况,处理参数和栈的变化。最终如果栈和原序列都取空了,把序列加到可能序列当中。最终输出数量即可。

本做法效率较低(因为只能应用当前知识点)。更快速的算法参照洛谷题解。

3.2 台阶问题

例题二

题目描述

N N N 级台阶,你一开始在底部,每次可以向上迈 1 ∼ K 1\sim K 1K 级台阶,问到达第 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 1N10 1 ≤ K ≤ 3 1\leq K\leq3 1K3
  • 对于 40 % 40\% 40% 的数据, 1 ≤ N ≤ 1000 1\leq N\leq1000 1N1000
  • 对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 100000 1\leq N\leq100000 1N100000 1 ≤ K ≤ 100 1\leq K\leq100 1K100

做法

本题没有明显的递推式,需要我们自己推导。

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} fij。由于决策不同,方案数应该加起来,即 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)fij

时间复杂度 O ( n k ) O(nk) O(nk),可以通过此题。

4 习题

禽兽的传染病
月落乌啼算钱(斐波那契数列)
[NOIP2001 普及组] 数的计算
迷宫

其中一、二题较简单,三、四题较难,建议先独立思考,不会再看题解。

结语

递推与递归是重要基础,是后面搜索和动态规划基础,搜索和动态规划又是整个算法竞赛的两个重要部分。只有打下坚实的基础,才能在算法竞赛的道路上走得更远。


http://www.kler.cn/a/511019.html

相关文章:

  • lvm快照备份
  • Django简介与虚拟环境安装Django
  • [JavaScript] 运算符详解
  • 第17章:Python TDD回顾与总结货币类开发
  • 安全测评主要标准
  • 汽车网络信息安全-ISO/SAE 21434解析(上)
  • Rust 的核心工具链
  • JS宏进阶:正则表达式的使用
  • windows蓝牙驱动开发-BLE音频(二)
  • ROS2 与机器人视觉入门教程(ROS2 OpenCV)
  • 高通8255 Android STR 启动失败要因分析调查
  • Linux 常用命令——文件目录篇(保姆级说明)
  • MySQL中的GROUP_CONCAT函数将分组后的多个行值合并成一个字符串,并用指定分隔符连接
  • 《论文阅读》通过思维链微调产生原因感知的同理心反应 2024
  • 规避路由冲突
  • 【Vim Masterclass 笔记21】S09L39:Vim 设置与 vimrc 文件的用法示例(二)
  • AI守护煤矿安全生产:基于视频智能的煤矿管理系统架构解析
  • Python在DevOps中的应用:自动化CI/CD管道的实现
  • 从C到C++:嵌入式开发中两者的差异与过渡技巧
  • C# 声明废弃特性
  • 使用 Go 语言生成样式美观的 PDF 文件
  • LeetCode - #187 Swift 实现重复的DNA序列
  • 事务处理系统 (Transaction Processing System, TPS)
  • TCP报文格式与核心机制
  • Pycharm报错:libpng warning: iCCP: known incorrect sRGB profile
  • [手机Linux] 七,NextCloud优化设置