news 2026/10/29 3:27:36

动态规划入门:斐波那契与爬楼梯问题解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
动态规划入门:斐波那契与爬楼梯问题解析

1. 动态规划入门三部曲:从斐波那契到爬楼梯问题

最近在代码随想录算法训练营第三十八天的内容中,我们遇到了三个经典的动态规划入门题目:509.斐波那契数、70.爬楼梯和746.使用最小花费爬楼梯。这三个题目看似简单,却完美诠释了动态规划的核心思想。作为算法学习路上的必经关卡,它们值得每个程序员深入理解。

我在最初学习动态规划时,也曾被各种复杂的状态转移方程吓到,直到遇到这三个"友好"的题目才恍然大悟。今天,我就从实际编码的角度,带大家拆解这三个问题的解法,分享一些只有踩过坑才能获得的经验。无论你是刚开始刷题的新手,还是想巩固基础的进阶者,这篇文章都能给你带来直接可用的解题思路和优化技巧。

2. 斐波那契数:动态规划的Hello World

2.1 问题描述与递归解法

斐波那契数列是理解递归和动态规划最经典的例子。题目要求我们实现一个函数,计算第n个斐波那契数。斐波那契数列的定义非常简单:F(0) = 0,F(1) = 1,对于n > 1的情况,F(n) = F(n-1) + F(n-2)。

最直观的解法就是递归:

def fib(n): if n <= 1: return n return fib(n-1) + fib(n-2)

这个解法虽然简洁,但存在严重的性能问题。我曾在本地测试计算fib(40),结果等了近10秒才出结果。原因在于递归树中存在大量重复计算,时间复杂度达到了惊人的O(2^n)。

提示:在面试中,如果直接给出这种递归解法而不指出问题,可能会给面试官留下对算法复杂度不敏感的负面印象。

2.2 记忆化搜索优化

解决重复计算最直接的方法就是加入缓存,也就是记忆化搜索(Memoization)技术:

def fib(n, memo={}): if n <= 1: return n if n not in memo: memo[n] = fib(n-1, memo) + fib(n-2, memo) return memo[n]

这个优化将时间复杂度降到了O(n),因为每个子问题只需要计算一次。空间复杂度也是O(n),因为需要存储所有中间结果。我在实际测试中发现,计算fib(40)的时间从10秒降到了几乎可以忽略不计的程度。

2.3 动态规划解法

记忆化搜索是自顶向下的解法,而动态规划通常是自底向上的。我们可以使用一个数组来存储中间结果:

def fib(n): if n <= 1: return n dp = [0] * (n + 1) dp[1] = 1 for i in range(2, n + 1): dp[i] = dp[i-1] + dp[i-2] return dp[n]

这种解法的时空复杂度都是O(n)。但在实际应用中,我们还可以进一步优化空间复杂度。

2.4 空间优化技巧

观察状态转移方程,我们发现当前状态只依赖于前两个状态,因此不需要存储整个数组:

def fib(n): if n <= 1: return n a, b = 0, 1 for _ in range(2, n + 1): a, b = b, a + b return b

这个版本的空间复杂度降到了O(1),是面试中最推荐的写法。我在刷题时发现,很多动态规划问题都可以用类似的滚动数组技巧优化空间。

3. 爬楼梯问题:斐波那契的变种

3.1 问题描述与建模

爬楼梯问题是这样的:假设你正在爬楼梯,每次可以爬1或2个台阶。问有多少种不同的方法可以爬到第n阶。

我第一次看到这个问题时,并没有立刻意识到它和斐波那契数的联系。直到画出几阶楼梯的可能情况后才发现规律:

  • 到第1阶:1种方法(1)
  • 到第2阶:2种方法(1+1, 2)
  • 到第3阶:3种方法(1+1+1, 1+2, 2+1)
  • 到第4阶:5种方法...

这明显就是斐波那契数列!状态转移方程为:dp[n] = dp[n-1] + dp[n-2]

3.2 标准动态规划解法

基于这个发现,我们可以写出和斐波那契数几乎相同的代码:

def climbStairs(n): if n <= 2: return n dp = [0] * (n + 1) dp[1] = 1 dp[2] = 2 for i in range(3, n + 1): dp[i] = dp[i-1] + dp[i-2] return dp[n]

同样,我们也可以应用空间优化:

def climbStairs(n): if n <= 2: return n a, b = 1, 2 for _ in range(3, n + 1): a, b = b, a + b return b

3.3 扩展思考:如果步长变化怎么办?

在实际面试中,问题可能会变化。比如,如果每次可以爬1、2或3个台阶,解法会怎样变化?这时状态转移方程就变成了: dp[n] = dp[n-1] + dp[n-2] + dp[n-3]

这个变种考察的是能否灵活应用动态规划思想,而不是死记硬背。我在面试辅导中发现,很多同学能解原题但遇到变种就束手无策,原因在于没有真正理解动态规划的本质。

4. 最小花费爬楼梯:动态规划的实用案例

4.1 问题描述与状态定义

746题是这样的:给定一个整数数组cost,其中cost[i]是从楼梯第i个台阶向上爬需要支付的费用。一旦支付此费用,可以选择向上爬一个或两个台阶。可以从下标为0或1的台阶开始爬楼梯,求达到楼梯顶部的最小花费。

这个问题比前两个更贴近实际应用场景。关键在于正确定义状态。经过几次尝试,我发现可以定义dp[i]为到达第i阶时的最小总花费。

4.2 状态转移方程推导

根据题意,到达第i阶有两种方式:

  1. 从i-1阶花费cost[i-1]上来
  2. 从i-2阶花费cost[i-2]上来

因此,状态转移方程为: dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])

初始条件是dp[0] = dp[1] = 0,因为我们可以选择从这两阶开始而不需要任何花费。

4.3 完整实现代码

基于以上分析,我们可以写出如下代码:

def minCostClimbingStairs(cost): n = len(cost) dp = [0] * (n + 1) for i in range(2, n + 1): dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]) return dp[n]

同样,这个解法也可以进行空间优化:

def minCostClimbingStairs(cost): a = b = 0 for i in range(2, len(cost) + 1): a, b = b, min(a + cost[i-2], b + cost[i-1]) return b

4.4 常见错误与调试技巧

在解决这个问题时,我最初犯过几个典型错误:

  1. 错误地将dp[i]定义为从第i阶开始的最小花费,导致状态转移混乱
  2. 忽略了可以从0或1阶开始的条件,初始条件设置错误
  3. 数组长度处理不当,忘记结果是dp[n]而不是dp[n-1]

调试这类问题时,我建议:

  • 先手动计算小例子(如cost=[10,15,20])
  • 打印出整个dp数组检查是否符合预期
  • 特别注意数组边界条件

5. 动态规划解题的通用方法论

通过这三个题目,我们可以总结出解决动态规划问题的通用步骤:

  1. 确定dp数组及其下标的含义
  2. 推导状态转移方程
  3. 初始化dp数组
  4. 确定遍历顺序
  5. 举例推导dp数组(用于验证)

在实际刷题中,我发现很多同学卡在第1步和第2步。我的经验是:

  • 先尝试暴力递归解法,观察是否存在重叠子问题
  • 画图帮助理解问题,特别是对于二维dp问题
  • 从小规模例子入手,寻找规律
  • 不要急于写代码,先确保状态定义正确

6. 性能对比与进阶思考

为了更直观地理解不同解法的性能差异,我做了如下测试(计算fib(40)):

方法时间复杂度空间复杂度实际运行时间
朴素递归O(2^n)O(n)~10秒
记忆化搜索O(n)O(n)<1毫秒
标准DPO(n)O(n)<1毫秒
优化DPO(n)O(1)<1毫秒

对于更复杂的问题,动态规划的优势会更加明显。比如背包问题、最长公共子序列等,暴力解法完全不可行,而动态规划能在合理时间内解决。

7. 常见问题与解决方案

在指导他人学习动态规划时,我收集了一些常见困惑和解决方法:

Q: 如何判断一个问题是否能用动态规划解决? A: 通常具有"最优子结构"和"重叠子问题"特性的问题适合DP。可以先尝试分治法,如果发现重复计算,就考虑DP。

Q: 状态转移方程总是想不出来怎么办? A: 尝试先列举小规模例子,寻找规律。也可以先写出暴力递归,再观察如何优化。

Q: 边界条件经常出错怎么办? A: 特别关注dp数组的初始化和索引范围。建议先明确dp[i]的定义,再处理边界。

Q: 如何从二维DP降维到一维? A: 观察状态转移是否只依赖于有限的几个状态。如果是,可以用滚动数组优化。

8. 实战练习建议

为了真正掌握动态规划,我建议的练习路径是:

  1. 先熟练掌握这三个基础题目
  2. 尝试类似难度的题目(如198.打家劫舍)
  3. 逐步过渡到更复杂的问题(如背包问题)
  4. 最后挑战hard级别的DP问题

在代码随想录的算法训练营中,题目顺序就是这样精心设计的。我自己的经验是,每个DP问题至少做两遍:第一遍理解思路,第二遍独立实现并优化。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 3:27:14

802.15.4z-2020 UWB测距标准解析:STS、TWR与工程避坑

简介&#xff1a;IEEE Std 802.15.4z-2020 标准原文面向低速率无线网络、UWB 测距与实时定位系统&#xff08;RTLS&#xff09;方向的研发人员、标准工程师及高校研究者&#xff0c;可用于查阅增强型超宽带物理层与相关测距技术的规范条文&#xff0c;为协议实现、射频设计与测…

作者头像 李华
网站建设 2026/10/29 3:27:13

Mask2Former与Swin Transformer在城市景观分割中的应用

1. 项目概述&#xff1a;Mask2Former-Swin城市景观分割方案这个项目本质上是在探索如何将Mask2Former这个通用分割框架与Swin Transformer这个强大的视觉骨干网络结合起来&#xff0c;专门用于处理城市景观这类复杂场景的图像分割任务。我在实际部署这个组合方案时发现&#xf…

作者头像 李华
网站建设 2026/10/29 3:27:13

Agent-Reach架构解析:智能体协作的连接、注册与路由基础设施

从去年开始&#xff0c;我一直在做一个偏底层的智能体基础设施项目&#xff0c;代号"Agent-Reach"。这个项目的起因很朴素&#xff1a;智能体&#xff08;Agent&#xff09;单体演示都好做&#xff0c;但一旦到了多智能体协作、跨部门复用、甚至跨团队交接的场景&…

作者头像 李华
网站建设 2026/10/29 3:26:34

Agent Skills 实战指南:从原理到落地,构建可复用的智能体能力模块

1. 从"skills"这个热词说起&#xff1a;它到底在解决什么问题最近一段时间&#xff0c;"skills"这个词在技术社区里的出现频率高得有点反常。如果你只是偶尔刷一刷动态&#xff0c;可能会以为这又是哪个新出的前端框架或者某个编辑器插件。但真正翻过几篇讨…

作者头像 李华
网站建设 2026/10/29 3:26:30

Agent-Reach:轻量级多Agent协调与触达层架构设计实践

最近一两个月&#xff0c;我一直在断断续续地搞一个名叫Agent-Reach的内部小项目。起因很简单&#xff0c;团队里Agent越来越多&#xff0c;有的负责数据采集&#xff0c;有的做文本处理&#xff0c;有的专门调用外部API&#xff0c;彼此之间却像孤岛一样&#xff0c;互相不知道…

作者头像 李华
网站建设 2026/10/29 3:26:15

界面渲染原理与性能优化实战指南

1. 界面渲染的本质与核心挑战界面渲染是将抽象的数据结构转化为用户可感知的视觉元素的过程。这个过程就像舞台剧的幕后工作——编剧的剧本&#xff08;数据结构&#xff09;需要经过导演的调度&#xff08;布局计算&#xff09;、演员的表演&#xff08;图形绘制&#xff09;、…

作者头像 李华