侧边栏壁纸
  • 累计撰写 192 篇文章
  • 累计创建 2 个标签
  • 累计收到 87 条评论

【基础算法】递推算法

Allen Best
2022-11-25 / 0 评论 / 0 点赞 / 79 阅读 / 8,798 字
温馨提示:
本文最后更新于 2023-04-27,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

递推算法

递推算法能通过已知某个条件,利用特定的关系得出中间推论,然后逐步递推,直到得到结果为止。由此可见,递推算法要比枚举算法“聪明”,它不会"一根筋"的寻找每一种可能方案。

(1)顺推法:从已知条件出发,逐步推算出要解决的方法。例如裴波那契数列就可以通过顺推法不断推算出新的数据。

(2)逆推法:从已知的结果出发,用迭代表达式逐步推算出问题开始的条件,即顺推法的逆过程。

无论顺推还是逆推,其关键是要找到递推式。这种处理问题的方法能使复杂运算化为若干步重复的简单运算,充分发挥出计算机擅长于重复处理的特点。

递推法是一种重要的数学方法,在数学的各个领域中都有广泛的运用,也是计算机用于数值计算的一个重要算法。

递推算法的首要问题是得到相邻的数据项间的关系(即递推关系)。递推算法避开了求通项公式的麻烦,把一个复杂的问题的求解,分解成了连续的若干步简单运算。一般说来,可以将递推算法看成是一种特殊的迭代算法。

一、斐波那契问题
数学里面的斐波那契数列便是一个使用递推算法的经典例子。13世纪意大利数学家斐波那契的《算盘书》中记载了典型的兔子产仔问题,其大意如下:如果一对两个月大的兔子以后每-一个月都可以生一对小兔子,而一对新生的兔子出生两个月后才可以生小兔子。也就是说,1月份出生,3月份才可产仔。那么假定一年内没有产生兔子死亡事件,那么1年后共有多少对兔子呢?
分析:我们先来分析一下兔子产仔问题。我们来逐月看一次每月的兔子对数:
第一个月: 1对兔子;

第二个月: 1对兔子;

第三个月: 2对兔子;

第四个月: 3对兔子;

第五个月: 5对兔子;

从上面可以看出,从第个3月开始,每个月的兔子总对数等于前两个

月兔子数的总和。相应的计算公式如下:

第n个月兔子总数F=Fn-1+Fn-2

这里,初始第一个月的兔子数为F1=1,第二个月的兔子数为F2=1.

二、汉诺塔(Tower of Hanoi)问题
移动规则:

每次只能移动一个圆盘;圆盘可以插在X、 Y和Z中的任何一个塔座上;任何时刻都不能将一个较大的圆盘压在较小的圆盘之上

假设移动n个盘子从A到C需要移动的次数为f(n)

f(0)=0

f(1)=1 临界条件

f(n)=f(n-1)+1+f(n-1)=2*f(n-1)+1 递推式【把n-1从A->B ,把第n个从A->C,把n-1从B->C】

#include<iostream>
using namespace std;
int main()
{
    int f[101],n;
    cin>>n;
    f[0]=0;f[1]=1;
    for(int i=2;i<=n;i++)
        f[i]=2*f[i-1]+1 ;
    cout<<f[n]<<endl;
    return 0;
}

三、猴子吃桃
猴子第一天采摘了一些桃子,第二天吃了第一天的一半多一个,第三天吃了第二天的一半多一个...直到第十天就剩下一个。问:猴子第一天摘了多少桃子?

f[n]代表第n天剩余的桃子的个数

f[n]=f[n-1]*1/2-1 -> f[n-1]=(f[n]+1)*2 递推式 、逆推

f[10]=1 临界条件

#include<iostream>
using namespace std;
int main()
{
    int f[101];
    f[10]=1;
    for(int i=9;i>=1;i--)
        f[i]=(f[i+1]+1)*2;
    cout<<f[1]<<endl;
    return 0;
}

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

#include<cstdio>  
long long d[110]= {0};  
int main()
{  
    d[1]=1;
    d[2]=2;
    d[3]=4;  
    for(int i=4; i<=100; i++)
        d[i]=d[i-1]+d[i-2]+d[i-3];  
    int n;
    cin>>n;
    cout<<d[n]<<endl;
    return 0;  
}  

四、数字三角形(顺推法)
请编一个程序计算从顶到底的某处的一条路径,使该路径所经过的数字总和最大。只要求输出总和。

一步可沿左斜线向下或右斜线向下走;
三角形行数小于等于100;
三角形中的数字为0,1,…,99;

二维数组存储,格式如右图

f[x][y]表示从(1,1)到(x,y)的路径的最大权值和

f[1][1]=a[1][1] 临界条件

f[x][y]=max{f[x-1][y],f[x-1][y-1]} 递推式

#include <iostream>
using namespace std;
int a[101][101]={0}; //注意此时二维数组的初值为0
int f[101][101],n;
int max(int a,int b){
if(a>b)
        return a;
return b;
}
int main()  {
cin >> n;
for(int i = 1;i <= n;i ++)
    for(int j = 1;j <= i;j ++)
        cin >> a[i][j];
f[1][1] = a[1][1];
//求出f[2][1],f[2][2]......f[5][5] 
for(int i = 2;i <= n;i ++)
    for(int j = 1;j <= i;j ++)
        f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
   int ans =0;
//这个循环求f[5][1]......f[5][5]的大小
for(int i = 1;i <= n;i ++)
    ans = max(ans,f[n][i]);
   cout << ans << endl;
return 0;
  }

五、骨牌铺满方格
有 2n 的一个长方形方格,用一个12 的骨牌铺满方格。
编写一个程序,试对给出的任意一个n(n>0), 输出铺法总数。

算法分析:

(1)当n=1时,

只能是一种铺法,铺法总数有示为x1=1。

(2)当n=2时,

骨牌可以两个并列竖排,也可以并列横排,再无其他方法,如下左图所示,因此,铺法总数表示为x2=2;

当n=3时,如上图
当n=4时,如下图

算法分析:

推出一般规律:对一般的n,xn可以这样来考虑

若第一个骨牌是竖排列放置

 剩下有n-1个骨牌需要排列,这时排列方法数为xn-1;

若第一个骨牌是横排列放置

 整个方格至少有2个骨牌是横排列(1*2骨牌),因此剩下n-2个骨牌需要排列,这是骨牌排列方法数为xn-2

假设f(n)表示n列的铺发总数

F(1)=1

F(2)=2

F(n)=f(n-1)+f(n-2)

#include <iostream>
using namespace std;
int main(){
    int n;//代表有几列
    cin>>n;
int f[101];
f[1]=1;f[2]=2;
for(int i=3;i<=n;i++)
        f[i]=f[i-1]+f[i-2];
    cout<<f[n]<<endl;
}

六、吃糖果
名名的妈妈从外地出差回来,带了一盒好吃又精美的巧克力给名名(盒内共有 N 块巧克力,20 > N >0)。
妈妈告诉名名每天可以吃一块或者两块巧克力。假设名名每天都吃巧克力,问名名共有多少种不同的吃完巧克力的方案。
分析:
每天只有两个选择,吃1块或者吃2块
f[n]: 表示吃完 n 块巧克力的方案数
n=1,1种方案 f[1]=1
n=2,2种方案 则名名可以第1天吃1块,第2天吃1块,也可以第1天吃2块, f[2]=2
.......

如果n= i,第一天要么吃掉1块,要么吃掉2块,所以剩余 i-1 块或者 i-2 块,因此,

 f[i] = f[i - 1] + f[i - 2]         递推式

七、蜜蜂路线
一只蜜蜂在下图所示的数字蜂房上爬动,已知它只能从标号小的蜂房爬到标号大的相邻蜂房,现在问:蜜蜂从蜂房m开始爬到蜂房n,m<n,有多少种爬行路线?

需要注意,斐波那契额数列是从1开始进的,现在是从m开始进,到n点。相当于走了 n-m 个点 f[n-m] ,本题涉及高精度算法

#include <cstdio>
using namespace std;
int n,m,len=1;
int f[1005][1005];
void plus(int x)
{
	for(int i=1;i<=len;i++)
	  f[x][i]=f[x-1][i]+f[x-2][i];
	for(int i=1;i<=len;i++)
	  if(f[x][i]>9)
	  {
	  	f[x][i+1]+=f[x][i]/10;
	  	f[x][i]%=10;
	  }
	if(f[x][len+1]) len++;
}
int main ()
{
	scanf("%d%d",&m,&n);
	f[1][1]=1,f[2][1]=2;
	for(int i=3;i<=n-m;i++) plus(i);
	for(int i=len;i;i--) printf("%d",f[n-m][i]);
	return 0;
}

八、昆虫繁殖
科学家在热带森林中发现了一种特殊的昆虫,这种昆虫的繁殖能力很强。

每对成虫过x个月产y对卵,每对卵要过两个月长成成虫。

假设每个成虫不死,第一个月只有一对成虫,且卵长成成虫后的第一个月内不产卵(过X个月产卵).

问过Z个月以后,共有成虫多少对?1=<x<=20,1<=y<=20,x=<z<=50

本月成虫的数量=前2个月卵的数量+前1个月成虫的数量

本月卵的数量=前x月成虫的数量*y

设f[i]表示第 i 月昆虫成虫的数量,b[i]表示第 i 月的新虫卵的数目

b[i] = f[ i - x] * y; (成虫经过x月产卵 y对)
f[i] = f[ i - 1] + b[ i - 2]; (卵经过2个月长成成虫)

F[1]=1 b[1]=0

#include <iostream>

using namespace std;

int main(){
    int x,y,z;

    cin>>x>>y>>z;

int f[101],b[101];

//在x个月之内成虫都是,都不产卵,所以赋初值一定是用循环

    for(int i=1;i<=x;i++){
       f[i]=1;

       b[i]=0;

  }

for(int i=x+1;i<=z+1;i++)

        //因为要统计到第z个月后,所以要for到z+1

    {
        b[i]=y*f[i-x];

        f[i]=f[i-1]+b[i-2];

    }

    cout<<f[z+1]<<endl;

    return 0;

}

九、分苹果
把 m 个同样的苹果放在 n 个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?

说明:5,1,1和1,5,1 是同一种分法。

【输入格式】一行,包含二个整数 m 和 n,以空格分开。1<=m,n<=10。

【输出格式】用一行输出相应的K。
设f(m,n) 为m个苹果,n个盘子的放法数目

当n>m:至少有n-m个盘子永远空着 即f(m,n) = f(m,m)
当n<=m:
有盘子空着,至少一个盘子空着 f(m,n) = f(m,n-1)

所有盘子都有苹果,多出m-n个苹果,拿走不影响放法的个数 f(m,n)=f(m-n,n)

故当n<=m时,f(m,n)=f(m,n-1)+f(m-n,n)

当没有苹果可放时,即m=0时,定义为1种放法;

边界条件: f[m,1]=1 f[i,0]=0 f[0][n]=1

#include <iostream> 
using namespace std;
int main(){
    int m,n,f[11][11];//第一维表示苹果个数,第二维表示盘子个数
    cin>>m>>n;
    //初始化如果盘子n的个数是0或1时初始化
    for(int i=0;i<=m;i++)
    {
        f[i][0]=0;
        f[i][1]=1;
    }
    //初始化如果苹果个数为0
    for(int i=0;i<=n;i++)
        f[0][i]=1;
    for(int i=1;i<=m;i++)//i是苹果个数
    {
        for(int j=1;j<=n;j++)//j是盘子个数
        {
            if(i<j)
                f[i][j]=f[i][i];
            else
                f[i][j]=f[i][j-1]+f[i-j][j];
        }
    }
    cout<<f[m][n]<<endl;
}

十、台阶问题

题目描述

有$N$级的台阶,你一开始在底部,每次可以向上迈最多$K$级台阶(最少$1$级),问到达第$N$级台阶有多少种不同方式。

输入格式

两个正整数N,K。

输出格式

一个正整数,为不同方式数,由于答案可能很大,你需要输出$ans \bmod 100003$后的结果。

样例 #1

样例输入 #1

5 2

样例输出 #1

8

提示

对于$20%$的数据,有$N ≤ 10, K ≤ 3$;

对于$40%$的数据,有$N ≤ 1000$;

对于$100%$的数据,有$N ≤ 100000,K ≤ 100$。

	#include<iostream>
using namespace std;
int main()
{
    int a[1000001]={1},i,j,n,k;
    cin>>n>>k;
    for(i=1;i<=n;i++)
    for(j=1;j<=k&&(i-j)>=0;j++)
    {
        a[i]=(a[i]+a[i-j])%100003;
    }
    cout<<a[n]<<endl;
    return 0;
}

十一、数的计算

题目描述

给出自然数 $n$,要求按如下方式构造数列:

  1. 只有一个数字 $n$ 的数列是一个合法的数列。
  2. 在一个合法的数列的末尾加入一个自然数,但是这个自然数不能超过该数列最后一项的一半,可以得到一个新的合法数列。

请你求出,一共有多少个合法的数列。两个合法数列 $a, b$ 不同当且仅当两数列长度不同或存在一个正整数 $i \leq |a|$,使得 $a_i \neq b_i$。

输入格式

输入只有一行一个整数,表示 $n$。

输出格式

输出一行一个整数,表示合法的数列个数。

样例 #1

样例输入 #1

6

样例输出 #1

6

提示

样例 1 解释

满足条件的数列为:

  • $6$
  • $6, 1$
  • $6, 2$
  • $6, 3$
  • $6, 2, 1$
  • $6, 3, 1$

数据规模与约定

对于全部的测试点,保证 $1 \leq n \leq 10^3$。

#include<bits/stdc++.h>//万能头文件
using namespace std;
int n;
int f[1001];//存每一位数的种类
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){ //1-n的递推
        for(int j=1;j<=i/2;j++){
            f[i]+=f[j]; //每一位叠加,递推走起
        }
        f[i]++; //加上本身
    }
    cout<<f[n];//输出n的种类
    return 0;
}

十二、旅行

题目描述

你要进行一个行程为7000KM的旅行,现在沿途有些汽车旅馆,为了安全起见,每天晚上都不开车,住在汽车旅馆,你手里现在已经有一个旅馆列表,用离起点的距离来标识,如下:

0, 990, 1010, 1970, 2030, 2940, 3060

3930, 4060, 4970, 5030, 5990, 6010, 7000

但在出发之前可能还要增加一些旅馆。

现在旅行社为了节约成本,要求每天至少行驶A公里,国家旅行社为了安全起见,要求每天最多只能行驶B公里。

你想知道一共有多少种旅行方案。

输入格式

第一行输入A,第二行输入B,第三行输入N(0≤N≤20),表示在出发之前又新增N个汽车旅馆;接下来N行,每行一个整数m,表示旅馆离起点的距离(0<m<7000)。注意:没有任意两个旅馆在同一位置。

输出格式

输出一共有多少种旅行方案。

样例 #1

样例输入 #1

500
1500
0

样例输出 #1

64
#include<bits/stdc++.h>
using namespace std;
int a,b,n,ans[40],r[40]={0,990,1010,1970,2030,2940,3060,3930,4060,4970,5030,5990,6010,7000};
//旅馆初始化 
int main(){
	scanf("%d %d %d",&a,&b,&n);
	for (int i=14;i<14+n;i++) cin>>r[i];//输入 
	sort(r,r+14+n);//排序 
	ans[0]=1; //起始点默认一套方案 
	for (int i=1;i<14+n;i++){//枚举所有点 
		for (int j=0;j<i;j++){//枚举这个点之前的点 
			if (r[i]-r[j]>=a&&r[i]-r[j]<=b){//如果这两个点之间的距离符合要求 
				ans[i]+=ans[j];//这个点可以获得前面那个点的所有可能 
			}
		}
	}
	cout<<ans[13+n]<<endl;//输出到终点时的所有可能 
	return 0;
}

十三、 踩方格

题目描述
有一个方格矩阵,矩阵边界在无穷远处。我们做如下假设:

a、每走一步时,只能从当前方格移动一格,走到某个相邻的方格上;

b、走过的格子立即塌陷无法再走第二次;

c、只能向北、东、西三个方向走;

请问:如果允许在方格矩阵上走n步,共有多少种不同的方案。2种走法只要有一步不一样,即被认为是不同的方案。

输入
允许在方格上行走的步数n(n≤20)。

输出
计算出的方案数量。

样例
输入数据 1
2
输出数据 1
7

其中f[i]表示的是走在方格上走i步的方法数;那么我们要怎么求f[n]呢,题目要求只能向北、东、西三个方向走(不能向南走,感觉这才是题目的关键)并且走过的格子立即塌陷无法再走第二次

解释:向北走后,格子塌陷,仍有三个方向选择;向东西方向走后,格子坍塌,只有两种选择(无法回头走)

第n-1步向北走的方法数(记为North[n-1]),那么North[n-1]其实就是在走了n-2步的情况下再向北走一步的的情况数,
所以North[n-1]=f[n-2],
有了North[n-1],那么第n-1步向东或西走的方法数也就有了(因为这两种情况加起来就是f[n-1]),既f[n-1]-f[n-2];
将这些数带入上图中得f[n]=f[n-2]* 3+(f[n-1]-f[n-2])* 2,化简得f[n]=f[n-1]*2+f[n-2]


十四、 数列

题目描述
用以下方式构造数列 数列的第一个和第二个数都为1,接下来每个数都等于前面2个数之和。 给出一个正整数a,要求数列中第a个数对1000取模的结果是多少。

输入
第1行是测试数据的组数n,后面跟着n行输入。每组测试数据占1行,包括一个正整数a(1 <= a <= 1000000)。

输出
n行,每行输出对应一个输入。输出应是一个正整数,为数列中第a个数对1000取模得到的结果。

样例
输入复制
4
5
2
19
1
输出复制
5
1
181
1

#include<iostream>
using namespace std;
#define SIZEN 1000000
int arr[SIZEN+9];//全局变量,默认初始化
int main() {
	int n,x,i;
	arr[1]=1;//第1项
	arr[2]=1;//第2项
	for(int i=3; i<=SIZEN; i++)
		arr[i]=(arr[i-1]+arr[i-2])%1000;//取模
	cin>>n;
	for(int i=1; i<=n; i++) {
		cin>>x;
		cout<<arr[x]<<endl;
	}
	return 0;
}

十五、装信问题
问题 :
某人写了n封信和n个信封,如果所有的信都装错了信封。求所有的信都装错信封,共有多少种不同情况?


对n封信以及n个信封各自按照从1到 n 进行编号,当 n 个编号的信放在 n 个编号位置的信封时,信的编号与信封位置编号 各不对应 的方法数用 f[n] 表示,那么 f[n - 1] 就表示 n - 1个编号的信放在 n - 1个编号位置的信封,各不对应的方法数,其他类推。

第一步,把第n封信放在一个信封,比如第k个信封 (k ≠ n) ,一共有 n - 1种方法。

第二步,放编号为 k 的信,这时有两种情况:

(1) 把它放到第 n 个信封,那么,对于剩下的 n - 2 封信,需要放到剩余的 n - 2个信封且各不对应,就有 f[n - 2]种方法。

(2) 不把它放到位置 n,这时,对于这 n - 1 封信,放到剩余的 n - 1个信封且各不对应(此时假设第k封信与位置n为对应关系),有 f[n - 1]种方法。

由于(1)与(2)是同一步骤的两种不同情况,所以第二步放法总数即为(1)与(2)这同一步骤的两种情况放法之和,即为 f[n - 2] + f[n - 1]。
归纳:

由以上分析可知,由于整个过程是由上述第一步、第二步两个步骤实现,因此 n 封信放到 n 个信封全部放错的方法就是上述第一步与第二步方法之积,即

{ 递推关系式  f[n] = (n - 1)  * ( f[n - 1] + f[n - 2] )  (n > 2)

 {   递推边界     f[1] = 0, f[2] = 1

#include <stdio.h>
#define N 50
int main()
{
    int i, n;
    long long int f[N + 1];
    f[1] = 0;
    f[2] = 1;
    scanf("%d", &n);
    for(i = 3; i < n; i++)
    {
        f[i] = (n - 1) * (f[i - 1] + f[i - 2]);
    }
    printf("%lld\n", f[n]);
    return 0;
}

0

评论区