[Python]动态规划三步走:从爬楼梯到打家劫舍,再到最大子数组和

前言

动态规划这块,我从一开始看到题目就懵,到现在能独立写出三道经典题,中间踩了不少坑。这篇文章把我真实的思考过程和犯错记录写下来,希望能帮到和我一样刚开始接触DP的同学。

第一步:爬楼梯 ------ 理解"最后一步分类"

题目:每次可以爬1级或2级台阶,爬到第n级有多少种不同的方法?

我的理解过程

一开始我看到递推公式 dp[i] = dp[i-1] + dp[i-2] 完全不知道是怎么来的。后来老师用"最后一步"的思路帮我拆解:

  • 要走到第i级台阶,最后一步要么是从第i-1级跨1步上来,要么是从第i-2级跨2步上来
  • 所以走到第i级的方法数 = 走到第i-1级的方法数 + 走到第i-2级的方法数

这个"最后一步分类"的思路是DP的核心思想之一:站在终点往前看,把问题拆成几种可能性

滚动变量的优化

基础版本用数组存储所有状态,空间复杂度O(n)。优化后只用三个变量滚动:

复制代码
def climbStairs(n):
    if n <= 2:
        return n
    a, b = 1, 2
    for i in range(3, n + 1):
        c = a + b
        a, b = b, c
    return b

老师当时打了个比方:就像滚雪球,a和b不断往前滚,旧的数字就被丢掉了。这个比喻让我一下就记住了。

关键领悟

DP的本质是反向思考、正向计算。我们从目标出发分析可能性,然后从起点开始一步步算到终点。

第二步:打家劫舍 ------ 理解"状态定义"

题目:一排房子,相邻不能同时偷,求能偷到的最大金额。

状态定义

dp[i] 表示偷到第i间房子时的最大金额。注意,不一定非要偷第i间。

递推公式的推导

对于第i间房子,有两种选择:

  • 不偷 :金额等于 dp[i-1]
  • :金额等于 dp[i-2] + nums[i](因为相邻不能偷,所以只能从i-2转移过来)

所以:dp[i] = max(dp[i-1], dp[i-2] + nums[i])

我踩的坑

我第一次写的时候,边界条件写成了:

复制代码
if n <= 1:
    return 1  # ❌ 应该是 return nums[0]

别笑,我当时真的以为返回1就行了。后来才发现,如果只有一个房子,应该返回它的金额,而不是固定值1。

滚动变量的应用

由于递推只依赖前两个状态,同样可以用滚动变量:

复制代码
def rob(nums):
    if not nums:
        return 0
    if len(nums) == 1:
        return nums[0]
    prev2, prev1 = nums[0], max(nums[0], nums[1])
    for i in range(2, len(nums)):
        curr = max(prev1, prev2 + nums[i])
        prev2, prev1 = prev1, curr
    return prev1

关键领悟

状态定义决定了递推公式的写法。定义清楚了,公式自然就出来了。

第三步:最大子数组和 ------ 最折磨人的一道

题目:给定整数数组,找出和最大的连续子数组,返回其和。

这道题为什么难

前两道题的递推公式都比较直观,但这道题的递推思路不太一样。它不是简单的"选或不选",而是要决定是否延续前面的子数组

状态定义

dp[i] 表示以 nums[i] 结尾的最大子数组和。注意,子数组必须包含 nums[i]

递推公式

复制代码
dp[i] = max(nums[i], dp[i-1] + nums[i])

翻译成人话就是:对于当前元素,要么重新开始 (只取当前元素),要么延续前面的子数组(带上前面一起)。

我反复踩的坑

这道题我折腾了很久,核心问题就是:在两个候选值里,我总是下意识选小的那个

比如数组 2 -1 4 -6 3,第1步 cur = -1,两个选项:

  • 延续:2 + (-1) = 1
  • 重新开始:-1

我选了-1。但明明1更大啊!后来老师给了我一句口诀:

前面是负,不如单干;前面是正,带上更猛。

这句话的意思是:如果上一步的 dp[i-1] 是负数,那延续只会拖累当前元素,不如重新开始;如果是正数,延续通常更有利。

全负数的情况

还有一个极端情况:数组全是负数。比如 -3 -1 -5 -2 -4

这时候每一步都应该选重新开始,因为延续只会让负数变得更小。最终答案就是数组中最大的那个负数(最接近0的那个)。

最终代码

复制代码
def maxSubArray(nums):
    prev = nums[0]
    result = nums[0]
    for num in nums[1:]:
        prev = max(num, prev + num)
        result = max(result, prev)
    return result

注意两点:

  1. 循环从第二个元素开始(nums[1:]),因为第一个元素已经在初始化时处理了
  2. result 只记录历史最大值,不是每一步都更新

一个有用的比喻

可以把这道题想象成做生意:

  • 每天都有一个利润(可能是正也可能是负)
  • 你可以选择今天重新开张 (只算今天的利润),也可以选择接着昨天的生意继续做(加上今天的利润)
  • 你的目标是找到某一段连续时间里的最大总利润
  • 如果昨天亏了,今天重新开张更明智;如果昨天赚了,接着做更划算

总结:DP三步法

经过这三道题的洗礼,我总结出DP的通用步骤:

  1. 定义状态 :搞清楚 dp[i] 代表什么
  2. 推导递推公式:站在第i步,思考可以从哪些状态转移过来
  3. 确定初始条件和边界:前几个状态怎么定,特殊值怎么处理

至于空间优化(滚动变量),那是锦上添花的事,先把基本逻辑跑通再说。

写在最后

说实话,最大子数组和这道题我差点放弃了。学了整整两天,反复算错,一度觉得自己不是学算法的料。但坚持下来之后发现,所谓的"顿悟"其实就是把同一个道理想通了无数遍之后,突然不再怀疑自己了。

相关推荐
默_笙1 天前
🍙 给每个请求过安检:FastAPI 是怎么把校验写进类型注解的
python
小羊没烦恼!1 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
qq_426003961 天前
启动playwright录制codegen生成自动化测试脚本
python·自动化
虎头金猫1 天前
4K 视频总卡在公网带宽?用 N1 + OpenList 把网盘播放链路重新理顺
运维·服务器·网络·python·容器·beautifulsoup·pandas
长沙三为智能科技1 天前
家政小程序开发从0到上线:五阶段交付流程与验收清单
python
伞伞悦读1 天前
【第38期】Python 模块与包详解:import、from、模块搜索路径、包结构和 __init__
开发语言·python
只睡四小时1 天前
Canvas 弹道联机实战:700 行 + 固定时间步长
python·websocket·html5·游戏开发·canvas
C语言小火车1 天前
C/C++ 为什么需要编译器?
开发语言·c++
奇思妙想聪明勤奋的小羊1 天前
DeepAgents第5章:子Agent 与上下文隔离—让 Agent学会委派
人工智能·python·学习·语言模型
lpfasd1231 天前
2026年第38周GitHub趋势周报
python·科技·github