Leetcode70——爬楼梯(斐波那契类型)(C语言)(通过该问题讲解动态规划基本思想)

本文主要是介绍Leetcode70——爬楼梯(斐波那契类型)(C语言)(通过该问题讲解动态规划基本思想),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

原题链接:        

动态规划的基本思想:

爬楼梯题目讲解:

解法1(递归):

解法2(记忆化递归):

解法3(Fibonacci数列的动态规划算法):

总结:


原题链接:        

原题链接:70. 爬楼梯 - 力扣(LeetCode)


动态规划的基本思想:

        动态规划(Dynamic Programming,简称DP)是一种算法设计技术,它通过将复杂问题分解为更小的子问题来解决优化问题。动态规划通常用于解决具有重叠子问题和最优子结构特性的问题。
        动态规划的基本思路可以概括为以下几个步骤:
                1. 定义状态:首先,需要定义问题中的状态。状态是指在特定步骤或阶段的问题的某种描述。通常,状态可以用一个或多个变量来表示,并且状态之间存在依赖关系。
                2. 状态转移方程:其次,确定状态转移方程,也称为递推关系。这个方程描述了状态如何从之前的某个状态或多个状态转移而来。在形式上,这个方程通常表示为 `dp[i] = f(dp[i-1], dp[i-2], ...)`,其中 `dp[i]` 是到达第 `i` 个状态的方法数或成本,而 `f` 是一个关于前状态的函数。
                3. 边界条件:确定初始状态或边界条件。这些是问题的基础情况,通常可以直接计算得到,或者由问题的性质直接给出。
                4. 计算顺序:确定状态的计算顺序。通常,状态的计算顺序应该能够充分利用之前计算的结果,避免重复计算。
                5. 构建DP表:根据状态转移方程和边界条件,构建一个DP表(或数组),并按顺序填充表中的每个状态。
                6. 输出结果:最后,根据DP表中的最终状态输出结果。


        动态规划的关键优点是它能够显著减少计算复杂度,将指数级问题转化为多项式级问题。这主要是因为动态规划避免了重复计算相同的状态,而是利用已计算的状态结果。


        动态规划适用于具有最优子结构特性的问题,即一个问题的最优解包含了其子问题的最优解。此外,动态规划也适用于具有重叠子问题的问题,即在求解问题的过程中,相同的子问题会被多次计算。通过存储这些子问题的解,动态规划可以有效地减少计算量。


爬楼梯题目讲解:

        Fibonacci(斐波那契)数列问题,它是一个简单而典型的分治问题,Fibonacci数列的递归方程表示为:

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

        其中,F(n) 表示数列的第n项,F(n-1) 表示数列的第n-1项,F(n-2) 表示数列的第n-2项。

        注意,这个递归方程是从第三项开始的,因为前两项有明确的定义:

        F(0) = 0 ;

        F(1) = 1。

解法1(递归):

#define _CRT_SECURE_NO_WARNINGS#include <stdio.h>
#include <stdlib.h>
int climbStairs(int n) {if (n < 0) return 0;if (n == 1) return 1;if (n == 2) return 2;return climbStairs(n - 1) + climbStairs(n - 2);
}
int main() {int n;printf("输入台阶数>");scanf("%d", &n);printf("有%d种方法可以爬到楼顶。", climbStairs(n));return 0;
}

        该程序实现简单,但非常低效。以求解该题目不难发现,在递归调用过程中,每次产生的子问题并不是新问题,有些子问题被反复计算多次,比如climbStairs(3)调用了两次,climbStairs(2)调用了三次,这种现象被称为 重叠子问题。

        当输入参数n较大时,其重复计算的子问题数目将爆照式增长,最终需要计算的子问题个数(或者递归调用次数)将增长为指数级。



那么该如何减少实际求解的子问题的数目呢?

        如果用一个数组保存已解决的子问题的答案,而在需要时再从该数组中查找已经求解的子问题的答案,这样就可以避免大量的重复计算,从而得到多项式复杂性算法。这就是动态规划的基本思路。


        记忆化(缓存):由于递归会多次计算相同的子问题,这会导致大量的重复计算,特别是在 n 较大时。为了避免重复计算,我们可以使用一个数组 arr 来存储已经计算过的结果。如果 arr[n] 已经被计算过,就直接返回它的值,否则就计算它并存储起来。

解法2(记忆化递归):

#define _CRT_SECURE_NO_WARNINGS#include <stdio.h>
#include <stdlib.h>int arr[10];int climbStairs(int n) {int tmp;if (n < 0) {return 0;}else if (arr[n] != 0) {return arr[n];}else {tmp = climbStairs(n - 1) + climbStairs(n - 2);return tmp;}
}int main() {int n, i;for (i = 0; i < 50; i++) {arr[i] = 0;}arr[1] = 1;arr[2] = 2;printf("输入台阶数>");scanf("%d", &n);printf("有%d种方法可以爬到楼顶。", climbStairs(n));return 0;
}

解法3(Fibonacci数列的动态规划算法):

#define _CRT_SECURE_NO_WARNINGS#include <stdio.h>
#include <malloc.h>// 动态规划解决爬楼梯问题
int climbStairs(int n) {if (n <= 2) {return n;}int* dp = (int*)malloc((n + 1) * sizeof(int));dp[1] = 1;dp[2] = 2;for (int i = 3; i <= n; i++) {dp[i] = dp[i - 1] + dp[i - 2];}int result = dp[n];free(dp); // 释放分配的内存return result;
}int main() {int n;printf("输入台阶数: ");scanf("%d", &n);printf("有 %d 种方法可以爬到楼顶。\n", climbStairs(n));return 0;
}

        该代码基本思路是:要到达第 `n` 阶,你可以从第 `n-1` 阶爬 1 阶上来,或者从第 `n-2` 阶爬 2 阶上来。因此,到达第 `n` 阶的方法数等于到达第 `n-1` 阶和第 `n-2` 阶方法数的和。

动态规划的步骤如下:
        1. 初始化:首先,我们需要定义一个数组 `dp` 来存储到达每一阶楼梯的方法数。由于到达第一阶和第二阶的方法数是固定的,我们可以直接初始化 `dp[1] = 1` 和 `dp[2] = 2`。
        2. 递推计算:然后,我们从第三阶开始,逐个计算到达每一阶的方法数。对于每一阶 `i`(`i` 从 3 开始到 `n`),它的方法数 `dp[i]` 等于前两阶方法数的和,即 `dp[i] = dp[i-1] + dp[i-2]`。
        3. 输出结果:最后,当计算完所有阶数后,`dp[n]` 就是到达楼顶的方法数。我们将这个值输出即可。
        在代码中,我们使用了一个循环来逐阶计算方法数,并在循环结束后返回 `dp[n]` 的值。在主函数中,我们接收用户输入的楼梯阶数 `n`,并调用 `climbStairs` 函数来计算并输出结果。
        注意,我们使用了 `malloc` 来动态分配内存,这是因为我们在编译时不知道 `n` 的值,因此不能使用静态数组。在使用完动态分配的内存后,我们使用 `free` 函数来释放内存,这是一个良好的编程习惯,可以防止内存泄露。


总结:

        Fibonacci数列的例子可以得到,动态规划的关键在于解决重叠子问题的重复计算,将原来指数级复杂度的分治算法改进多项式级的计算。

        在实现过程中,动态规划算法需要存储各子问题的解,所以它的空间复杂度大于其他算法,这是一种空间换时间的策略。

这篇关于Leetcode70——爬楼梯(斐波那契类型)(C语言)(通过该问题讲解动态规划基本思想)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/859697

相关文章

Go语言中make和new的区别及说明

《Go语言中make和new的区别及说明》:本文主要介绍Go语言中make和new的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 概述2 new 函数2.1 功能2.2 语法2.3 初始化案例3 make 函数3.1 功能3.2 语法3.3 初始化

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

Java 线程安全与 volatile与单例模式问题及解决方案

《Java线程安全与volatile与单例模式问题及解决方案》文章主要讲解线程安全问题的五个成因(调度随机、变量修改、非原子操作、内存可见性、指令重排序)及解决方案,强调使用volatile关键字... 目录什么是线程安全线程安全问题的产生与解决方案线程的调度是随机的多个线程对同一个变量进行修改线程的修改操

Redis出现中文乱码的问题及解决

《Redis出现中文乱码的问题及解决》:本文主要介绍Redis出现中文乱码的问题及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1. 问题的产生2China编程. 问题的解决redihttp://www.chinasem.cns数据进制问题的解决中文乱码问题解决总结

Go语言中nil判断的注意事项(最新推荐)

《Go语言中nil判断的注意事项(最新推荐)》本文给大家介绍Go语言中nil判断的注意事项,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1.接口变量的特殊行为2.nil的合法类型3.nil值的实用行为4.自定义类型与nil5.反射判断nil6.函数返回的

Go语言数据库编程GORM 的基本使用详解

《Go语言数据库编程GORM的基本使用详解》GORM是Go语言流行的ORM框架,封装database/sql,支持自动迁移、关联、事务等,提供CRUD、条件查询、钩子函数、日志等功能,简化数据库操作... 目录一、安装与初始化1. 安装 GORM 及数据库驱动2. 建立数据库连接二、定义模型结构体三、自动迁

ModelMapper基本使用和常见场景示例详解

《ModelMapper基本使用和常见场景示例详解》ModelMapper是Java对象映射库,支持自动映射、自定义规则、集合转换及高级配置(如匹配策略、转换器),可集成SpringBoot,减少样板... 目录1. 添加依赖2. 基本用法示例:简单对象映射3. 自定义映射规则4. 集合映射5. 高级配置匹

嵌入式数据库SQLite 3配置使用讲解

《嵌入式数据库SQLite3配置使用讲解》本文强调嵌入式项目中SQLite3数据库的重要性,因其零配置、轻量级、跨平台及事务处理特性,可保障数据溯源与责任明确,详细讲解安装配置、基础语法及SQLit... 目录0、惨痛教训1、SQLite3环境配置(1)、下载安装SQLite库(2)、解压下载的文件(3)、

全面解析MySQL索引长度限制问题与解决方案

《全面解析MySQL索引长度限制问题与解决方案》MySQL对索引长度设限是为了保持高效的数据检索性能,这个限制不是MySQL的缺陷,而是数据库设计中的权衡结果,下面我们就来看看如何解决这一问题吧... 目录引言:为什么会有索引键长度问题?一、问题根源深度解析mysql索引长度限制原理实际场景示例二、五大解决

Springboot如何正确使用AOP问题

《Springboot如何正确使用AOP问题》:本文主要介绍Springboot如何正确使用AOP问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录​一、AOP概念二、切点表达式​execution表达式案例三、AOP通知四、springboot中使用AOP导出