首页 > 编程语言 >C语言/C++——递归、递推、动态规划

C语言/C++——递归、递推、动态规划

时间:2025-01-21 22:28:59浏览次数:3  
标签:i64 数列 递归 C++ C语言 斐波 dfs 那契 递推

什么是动态规划:给定一个问题,我们把他拆成一个个子问题,直到子问题可以直接解决。然后把子问题的答案保存起来,以减少重复计算。再根据子问题的答案反推,得出原问题解的一种方法

递归的过程:"递"的过程是分解子问题的过程;(dfs是第归的一种)

                        “归”的过程是产生答案的过程

“递”的过程是自顶向下,

“归”的过程是自底向上,“底”代表的是已知最小子问题的答案

递归适用于以下情况:

1.  问题具有递归结构:问题可以自然地分解为更小的子问题,且子问题具有相同的结构,即即原问题可以通过解决一个或多个更小规模的同类问题来解决。 

2.  递归基和递归步骤清晰:可以明确地定义递归的终止条件和分解方式。  

3.  问题规模适中:递归深度不会过大,避免栈溢出。  

4.  代码可读性优先:递归实现更简洁,且性能优化可以通过记忆化等方式实现。

递归与栈有些相似:后进的先出,先进的后出

递归通常需要满足以下两个条件:(递归的本质就是:由已知推未知))

1.  递归基(Base Case):问题的最简单形式,可以直接求解,不需要进一步递归。递归基是递归的终止条件,防止无限递归。  

递归终止条件指的在程序中能实现return语句的条件

2.  递归步骤(Recursive Step):将原问题分解为更小规模的子问题,并通过递归调用自身来解决这些子问题。

递推:是递归的“归”的过程

递归与递推的联系:递推公式 = dfs向下递归的公式

递推数组的初始值 = 递归的边界

记忆化搜索=暴力dfs+记录答案(拿空间换时间)

最暴力的dfs——>记忆化搜素——>递推。

例题1

一个楼梯共有 n 级台阶,每次可以走一级或者两级,问从第 0 级台阶走到第 n 级台阶一共有多少种方案。

输入格式

共一行,包含一个整数 n。

输出格式

共一行,包含一个整数,表示方案数。

数据范围

1≤n≤41

样例

输入  10          输出  89
输入  19          输出  6765
输入  35          输出  14930352

此题本质就是斐波那契数列

long long可存下第101项

代码一(dfs暴力搜索,此法在n>41时,时间超限)(时间复杂度O(2^{n}))

#include<iostream>    
#include<algorithm>   
#include<cstring>     

using namespace std;  // 使用标准命名空间
using i64 = long long; // 定义一个别名i64,表示64位整数类型

int n;                 // 定义全局变量n,表示输入的斐波那契数列的索引

// 定义递归函数dfs,用于计算斐波那契数列的第x项
i64 dfs(int x)
{
    if(x == 1) return 1;                // 斐波那契数列的第1项为1,递归结束标志
    else if(x == 2) return 2;           // 斐波那契数列的第2项为2,递归结束标志
    else return dfs(x - 1) + dfs(x - 2); // 递归计算:第x项等于第x-1项与第x-2项之和
                                         // 深度优先搜索:先递归计算dfs(x-1),再计算dfs(x-2)
//直到搜索到x=1或x=2返回,在递推期间不返回答案
}

int main()
{
    cin >> n;                           // 从标准输入读取一个整数n
    i64 ans = dfs(n);                   // 调用dfs函数计算斐波那契数列的第n项
    cout << ans << endl;                // 输出结果
    return 0;                           // 程序正常结束
}

 代码二(dfs记忆化搜索)优化时间复杂度,记忆数组为全局变量,须达到数组索引相同值相同

#include<iostream>    // 包含输入输出流库
#include<algorithm>   // 包含算法库(虽然本代码未使用其中的算法)
#include<cstring>     // 包含字符串操作库(虽然本代码未使用其中的函数)

using namespace std;  // 使用标准命名空间
using i64 = long long; // 定义一个别名i64,表示64位long long 整数类型
const int N = 100;     // 定义一个常量N,表示记忆化数组的最大大小

int n;                 // 定义全局变量n,表示输入的斐波那契数列的索引
i64 mem[N];            // 定义一个全局数组mem,用于记忆化存储斐波那契数列的结果

// 定义递归函数dfs,用于计算斐波那契数列的第x项
int dfs(int x)
{
    if(mem[x]) return mem[x]; // 第二次计算第x项时,直接返回其值(记忆化)

    i64 sum = 0;              // 定义一个变量sum,用于存储计算结果
    if(x == 1) sum = 1;       // 斐波那契数列的第1项为1,递归结束条件
    else if(x == 2) sum = 2;  // 斐波那契数列的第2项为2,递归结束条件
                                //递归结束条件是指的是能实现return语句的条件
    else sum = dfs(x - 1) + dfs(x - 2); // 递归计算:第x项等于第x-1项与第x-2项之和
                                //递归调用语句之前每次调用都是必须要执行的,递推语句之后的,在 
                      //调用结束之后,才开始执行,此后语句的执行顺序同栈,最后一次调用的最先执行

    mem[x] = sum;             // 将第x项结果存储到mem[x]中,在第一次深度搜索时将完整的执行所有                    
                              //调用语句
    return sum;               // 返回计算结果
}

int main()
{
    scanf("%d", &n);          // 从标准输入读取一个整数n
    i64 answer = dfs(n);      // 调用dfs函数计算斐波那契数列的第n项
    printf("%lld\n", answer); // 输出结果
    return 0;                 // 程序正常结束
}

代码思路分析

  1. 斐波那契数列的定义
    斐波那契数列是一个经典的数列,定义如下:

    • F(1)=1

    • F(2)=2

    • F(n)=F(n−1)+F(n−2) (对于 n>2)

    本代码中,斐波那契数列的第1项为1,第2项为2,之后的每一项都是前两项的和。

  2. 递归实现
    代码通过递归函数dfs来计算斐波那契数列的第x项:

    • 如果x为1或2,直接返回1或2(递归终止条件)。

    • 否则,通过dfs(x-1) + dfs(x-2)递归计算第x项的值。

  3. 记忆化搜索(Memoization)
    为了避免递归过程中的重复计算,代码引入了一个数组mem来存储已经计算过的斐波那契数列的结果。

    • dfs函数中,如果mem[x]已经存储了值,则直接返回mem[x],避免重复计算。

    • 如果mem[x]未存储值,则计算结果后将其存储到mem[x]中,供后续调用使用。

    记忆化搜索大大提高了递归的效率,将时间复杂度从指数级(O(2^{n}))降低到线性级(O(n))。

  4. 主函数

    • 主函数从标准输入读取一个整数n,表示需要计算斐波那契数列的第n项。

    • 调用dfs(n)计算结果,并将结果存储到answer中。

    • 最后将结果输出到标准输出。

代码三(动态规划)

使用迭代方法计算斐波那契数列,避免递归调用。 模拟递归的“归” 

#include<iostream>    // 包含输入输出流库
using namespace std;  // 使用标准命名空间
using i64 = long long; // 定义一个别名i64,表示64位整数类型

const int N = 100;    // 定义一个常量N,表示数组dp的最大大小
i64 dp[N] = {0};      // 定义一个数组dp,用于存储斐波那契数列的值,并初始化为0

int main()
{
    int n;            // 定义一个变量n,表示输入的斐波那契数列的索引
    cin >> n;         // 从标准输入读取一个整数n
    dp[1] = 1;        // 初始化dp数组的第1项为1
    dp[2] = 2;        // 初始化dp数组的第2项为2
    for(int i = 3; i <= n; i++) // 从第3项开始,依次计算斐波那契数列的值
    {
        dp[i] = dp[i - 1] + dp[i - 2]; // 第i项等于第i-1项与第i-2项之和
    }
    cout << dp[n] << endl; // 输出斐波那契数列的第n项
    return 0;              // 程序正常结束
}

代码功能说明

  1. 斐波那契数列的动态规划实现

    • 使用一个数组dp来存储斐波那契数列的值,避免了递归实现中的重复计算问题。

    • 初始化dp[1]为1,dp[2]为2。

    • 通过循环,从第3项开始,依次计算斐波那契数列的值,直到第n项。

  2. 主函数

    • 从标准输入读取一个整数n,表示需要计算斐波那契数列的第n项。

    • 使用动态规划的方法计算斐波那契数列的第n项,并将结果存储在dp[n]中。

    • 将结果输出到标准输出。

代码四 优化空间复杂度

如果只需要计算第n项,可以只存储前两项的值,从而将空间复杂度优化到O(1)。

#include<iostream>    // 包含输入输出流库
using namespace std;  // 使用标准命名空间
using i64 = long long; // 定义一个别名i64,表示64位整数类型

int main()
{
    int n;            // 定义一个变量n,表示输入的斐波那契数列的索引
    cin >> n;         // 从标准输入读取一个整数n

    i64 a = 1, b = 2, c; // 定义三个变量:
                         // a表示斐波那契数列的第1项(F(1) = 1)
                         // b表示斐波那契数列的第2项(F(2) = 2)
                         // c用于存储当前计算的斐波那契数列的第i项

    // 特殊情况处理:直接输出F(1)或F(2)
    if(n == 1) cout << a << endl;  // 如果n为1,直接输出第1项(1)
    else if(n == 2) cout << b << endl; // 如果n为2,直接输出第2项(2)
    else
    {
        // 从第3项开始计算斐波那契数列
        for(int i = 3; i <= n; i++) // 循环计算从第3项到第n项
        {
            c = a + b;  // 当前项c等于前两项a和b的和
            a = b;      // 更新a为b(即F(i-2) = F(i-1))
            b = c;      // 更新b为c(即F(i-1) = F(i))
        }
        cout << c << endl; // 输出计算得到的第n项
    }
    return 0;              // 程序正常结束
}

标签:i64,数列,递归,C++,C语言,斐波,dfs,那契,递推
From: https://blog.csdn.net/2401_88591507/article/details/145271099

相关文章

  • C++11新特性之auto
    1.auto的作用C++11使用auto做自动类型推导。自动推导变量的类型,不需要手动指定。简单的类型可以手写,但一些复杂的容易写错或不知道变量是什么类型的则推荐使用auto。简化写代码的烦恼。2.auto的使用语法        autoname=value;根据value值的类型,自动推导name......
  • 数据结构之链表(linked list)代码实现(小白轻松懂,C语言版)
    一、前言:链表的简单介绍链表(LinkedList)是一种重要的线性数据结构,它以节点(Node)的形式存储数据,每个节点通过指针(或引用)指向下一个节点,从而形成一个动态的数据链条。与数组不同,链表的内存分配并不连续,因此具有更灵活的插入和删除操作,但在随机访问元素时效率相对较低。链表通......
  • 为什么要学习C++?
            在编程语言的广阔天地中,C++以其独特的魅力和强大的功能占据着重要的一席之地。尽管它并非新兴的热门语言,学习曲线也相对陡峭,但这丝毫没有阻挡开发者们对它的热情。那么,究竟为什么要学习C++呢?接下来,我们将深入探讨其中的缘由。一、卓越的性能表现    ......
  • C++类型转换总结
    类型转换隐式转换C++自动执行很多类型转换:将一种算术类型的值赋给另一种算术类型的变量时,C++将对值进行转换;表达式中包含不同的类型时,C++将对值进行转换;将参数传递给函数时,C++将对值进行转换。C++类型转换的规则初始化和赋值进行的转换扩展:将一个值赋给值取值范......
  • C语言的那点事第五篇:编程界的“外卖小哥”函数
    函数就像是编程界的“外卖小哥”,帮你把任务分解成小块,然后把结果送回来。别担心,我会用幽默的方式带你飞驰在这个奇妙的世界里。咱们开始吧!1.函数定义与调用外卖小哥的职责:想象一下,你每天都要做饭,但每次都从头开始,那得多累啊!函数就像是你的“外卖小哥”,帮你把任务分解成小......
  • 【2025】Visual Studio详细安装使用教程(C/C++编译器)零基础入门到精通,收藏这一篇就够了
    Part1VisualStudio2022简介微软在官方开发博客中宣布,于2021年夏季发布VisualStudio2022的首个预览版,2022版本更快、更易于使用、更轻量级,专为学习者和构建工业规模解决方案的人设计。64位版的VisualStudio不再受内存限制困扰,主devenv.exe进程不再局限于4GB,用户......
  • C++ 如何讲隐藏的函数释放出来
    如果有一个基类:classDog{public: virtual~Dog(){} voidshow(inta) { cout<<"我是一只狗!"<<a<<"岁"<<'\n'; } voidmysong() { cout<<"哈哈哈..."<<'\n'; }privat......
  • Ubuntu 22.04上编译安装C++ libconfig库
    一、前言libconfig是一个C/C++配置文件解析库,支持读取和写入配置文件。它使用了一种简单易懂的语法,非常适合用于各种项目的配置管理。本文将详细介绍如何在Ubuntu22.04上编译和安装libconfig库。二、环境准备在开始编译安装libconfig之前,需要确保系统已经安装了必要的开发工具......
  • C语言中的二维数组
    1.二维数组的定义类型说明符数组名 [常量表达式][常量表达式];(1).类型说明符      表示二维数组中数据元素的类型 (2).数组名          标识符 (3).[常量表达式][常量表达式]      第1维       第2维   ......
  • C++template模板
    目录函数模板(FunctionTemplate)示例:类模板(ClassTemplate)示例:模板参数(TemplateParameters)非类型模板参数示例:模板特化(TemplateSpecialization)示例:C++中的模板(Template)是一种强大的特性,允许程序员编写与类型无关的代码。模板可以用于函数和类,使得代码更加通用和可......