递推

算法笔记第二期

递推

概念

  • 所谓递推,指的是从已知的初始条件出发,依据某种特定的关系,逐次推导出所要求的各种中间结果,直至得到结果的算法。
  • 从已知条件出发逐步推到问题结果,此种方法叫顺推。从问题出发逐步推到已知条件,此种方法叫逆推。不管怎么推导,关键在于找到递推式。充分发挥计算机擅长重复处理的特点。
  • 递推方法是一种重要的数学方法,将一个复杂问题分解为连续若干的简单运算。

递推式

  • 把序列a0、a1、...、an,...,简单记为{an}。一个把an与某些ai(i < n)联系起来的等式叫做关于序列{an}的递推方程
  • 假定有了初值,和递推式,就可以确定这个唯一序列

举例说明

阶乘问题

n的阶乘等于1×2×3...×n。其中n为正整数,假定f(n)表示n的阶乘。根据阶乘的计算方法可以得出

f(n) = 1 × 2 × 3 ... × n

f(n) = (1 × 2 × 3 ... × (n-1)) × n = f(n-1) × n

而初值 f(1) = 1. 根据初值和上面的递推式可以得出这个序列中的每一个值。

问题分析

例1、兔子繁殖问题

这是一个有趣的古典数学问题:有一对兔子,从出生后一个月就可以长大成熟,再过一个月就可以生育一对小兔子,并且之后每个月都能生育一对小兔子。

假设有一对刚出生的小兔子,问10个月后的兔子总数为多少?(假设所有兔子都不死)

分析

我们可以将每个月份开始时,兔子的情况列出如下表格

月份 1 2 3 4 5 6
幼兔 1 0 1 1 2 3
成兔 0 1 1 2 3 5
总数 1 1 2 3 5 8

根据分析表格很容易看出幼兔经过一个月后成为成兔,成兔的数量为上一个月成兔加上幼兔的数量(即上月总数),而幼兔的数量为上一个月成兔的数量。

T(n)表示第n个月幼兔的数,S(n)表示第n个月成兔的数量,f(n)表示第n个月总数

S(n) = T(n-1) + S(n-1) = f(n - 1)

T(n) = S(n-1) = T(n-2) + S(n-2) = f(n-2)

f(n) = T(n) + S(n) = f(n-1) + f(n-2)

也可以简单从如下角度考虑,第n个月的兔子数量,由两部分构成

1、上月存在的兔子数量f(n-1)

2、本月新出生的兔子f(n-2)

很明显,递推式已经得出,初值f(1) = 1,f(2) = 2

程序实现

/**
	顺推法
	根据初值和递推公式得到最终结果
**/
#include <cstdio>
long long a[50]; //斐波那契数列增长过快,因此使用long long数据类型
int main () {
	int n;
	a[1] = 2, a[2] = 2;
	scanf ("%d", &n);
	for (int i = 3; i <= n; i++) 
		a[i] = a[i-1] + a[i-2];
	printf("%lld\n", a[n]);
	return 0;
}
/**
	逆推法:
	一般是递归实现,从最终结果出发,不断缩小直至递归边界。
	方法效率过低。
**/
#include <cstdio>

long long fib (int n) {
	if (n == 1 || n == 2)
		return 1;
	return fib(n-1) + fib(n-2);
}

int main () {
	int n;
	scanf ("%d", &n);
	printf("%lld\n", fib(5));
	return 0;
}

记忆化操作

  • 使用递归求解问题的时候,通常效率低下,原因在重复计算

拿斐波那契数列举例,计算f(6)的过程如下图所示

明显发现除了边界,其他的中间值例如f(4)、f(3)等被重复计算多次,在我们计算f(5)的过程中实际上,f(4)、f(3)也被算了出来,在这种情况下,右边的f(4)依然在重复进行计算。要是能在在计算过程中,记住被计算过的中间值,这样操作就会大大增加效率。

程序如下

#include <cstdio>
#define N 100
long long bin[N]; //记忆化数组,初始化为0表示全部没有被计算过

long long fib (int n) {
	if (n == 1 || n == 2)
		return 1;
	if (!bin[n]) 
		bin[n] = fib(n-1) + fib(n-2); //每次计算都存储
	return bin[n];
}

int main () {
	int n;
	scanf ("%d", &n);
	printf("%lld\n", fib(n));
	return 0;
}

记忆化的操作通过空间换取了时间效率。将递归的中间值存放在数组中,当需要某个状态的值时,只需检查下数组中相应位置是否有值,如果有直接调用,如果没有再行计算。

小结

递推问题的关键点:

  • 问题可以按照事物的发展分成多个阶段状态
  • 除初始状态,每一个阶段都与前面的阶段有联系,可以根据递推关系式求得
  • 实现通常是顺序推算,或者记忆化。

训练

上台阶问题

【问题描述】

楼梯有n(71>n>0)阶台阶,上楼时可以一步上1阶,也可以一步上2阶,也可以一步上3阶,编程计算共有多少种不同的走法。

【输入】

输入的每一行包括一组测试数据,即为台阶数n。最后一行为0,表示测试结束。

【输出】

每一行输出对应一行输入的结果,即为走法的数目。

【输入样例】

1
2
3
4
0

【输出样例】

1
2
4
7

分析可得递推式f(n) = f(n-1) + f(n-2)+f(n-3)

原因在于,我们只考虑走最后一步的情况,最后一步实际上有三种走法,一种是走一步,一种是走两步,一种是走三步。走一步的总步数为f(n-1),走两步的总步数为f(n-2),走三步的总步数为f(n-3)。因此f(n) = f(n-1) + f(n-2) + f(n-3)。

代码

#include <cstdio>
long long f[80];
int main () {
	int n;
	f[1] = 1;
	f[2] = 2;
	f[3] = 4;
	while (1) {
		scanf ("%d", &n);
		if (n == 0) return 0;
		if (!f[n])
			for (int i = 4; i <= n; i++)
				f[i] = f[i-1] + f[i-2] + f[i-3];
		printf("%d\n", f[n]);
	}
	return 0;
}

将走楼梯的过程进行分解,走完n个楼梯通过若干动作来完成。如果只考虑最后一步走了多少楼梯,就可以实现一个降维操作。

二叉树个数

【问题描述】

二叉树是每个节点最多有两个子树的树形结构,且有左右子树之分,次序不能颠倒。给出二叉树的节点数n,请问有多少种不同的形态。

【输入】

一行N。(1<=n<=15)

【输出】

节点为n的二叉树共有多少种不同的形态

【样例输入】

3

【样例输出】

5

题目说明:

题目分析

不难发现,二叉树的左右子树其本质上也是一棵二叉树。并且左右子树的节点左右位置不同也会导致整棵树的形态不同。

假定f(n)表示n个节点的二叉树总共的形态数

现在来构造一棵二叉树,首先取出一个节点作为根节点,接下来构造左右子树,左边可以放0个节点,对应右边放n-1个节点,那么这种情况的总形态数就是f(0)×f(n-1),如果左边放1个节点右边放n-2个节点那么总形态数为f(1)×f(n-2)。因此总的形态数为f(n) = f(0) × f(n-1) + f(1) × f(n-2) + ... + f(n-1) × f(0)

而初值f(0) = 1, f(1) = 1。

参考程序

/**
	顺推
**/
#include <cstdio>
long long f[16];
int main () {
	int n;
	f[1] = f[0] = 1;
	scanf ("%d", &n);
	for (int i = 2; i <= n; i++) 
		for (int j = 0; j < i; j++)
			f[i] += f[j] * f[i-j-1];
	printf("%lld\n", f[n]);
}
/**
	记忆化
**/
#include <cstdio>
long long f[16];
long long function (int n) {
	if (n <= 1) return 1;
	if (!f[n]) 
		for (int i = 0; i < n; i++)
			f[n] += function(i) * function(n-1-i);
	return f[n];
}
int main () {
	int n;
	f[1] = f[0] = 1;
	scanf ("%d", &n);
	printf("%lld\n", function(n));
}

位数问题

【问题描述】

在所有的N位数中,有多少个数中有偶数个数字3?由于结果可能很大,你只需要输出这个答案对12345取余的值。

【输入】
读入一个数N(N≤1000)
【输出】
输出有多少个数中有偶数个数字3。
【输入样例】
2
【输出样例】
73

根据题目分析,假定f(n)表示位数为n的数中偶数个3的情况,t(n)表示位数为n的数中奇数个3的情况。

将问题的规模缩小一步考虑,实际上当前f[n]是由2部分组成,即n-1位数中所有偶数个3的情况乘以9(可以在每一位后面添加0、1、2、4、5、6、7、8、9),和n-1位数中所有奇数个3的情况乘以1(在后面添加3)。而奇数的3的情况也类似,那么递推式就出来了

f(n) = f(n-1) × 9 + t(n-1)

t(n) = t(n-1) × 9 + f(n-1)

需要注意的是,n=1,和n=2的情况单独考虑,因为n=1是0被考虑在内,而n=2的情况中,0是不能放在首位的。

参考程序

#include <cstdio>
#define N 1005
#define K 12345
int t[N], f[N];

int main () {
	int n;
	scanf ("%d", &n);
	t[1] = 1, f[1] = 9;
	t[2] = 17, f[2] = 73;
	for (int i = 3; i <= n; i++) {
		t[i] = (f[i-1] + t[i-1]*9) % K;
		f[i] = (t[i-1] + f[i-1]*9) % K;
	}
	printf("%d\n", f[n]);
}

总结

  • 递推方程实际上是一个数学方程。而我们是通过将问题划分为不同的阶段,找到各个阶段之间的练习的方式推导出来的。
  • 对于方程的推导。一般采用降维处理,考虑最后一步和哪些状态有联系。
  • 递推的很多公式推导都是乘法原理(分步完成)加法原理(分类完成)的使用。
posted @ 2020-08-21 14:00  S_K_P  阅读(1994)  评论(0)    收藏  举报