[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. 确定初始条件和边界:前几个状态怎么定,特殊值怎么处理

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

写在最后

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

相关推荐
AI视觉网奇18 分钟前
3d子部件高亮选中
开发语言·javascript·3d
风萧萧199920 分钟前
JAVA :JSONObject转换为XML
xml·java·python
海拥✘21 分钟前
网页抓取 API 稳定性怎么测?用 Dataify 完成一次真实压测
python
databook23 分钟前
几何分布:从“等一个结果”开始
python·数据挖掘·数据分析
zzzll111127 分钟前
深入理解 Java HashMap:从原理到实战
java·开发语言·哈希算法
猫猫不是喵喵.35 分钟前
Java开发高频面试题汇总(通俗易懂版)
java·开发语言·java高频面试
用户03321266636737 分钟前
如何在 Python 环境中裁剪 PDF 页面
python
威联通网络存储40 分钟前
TS-h2287XU-RP 在医药制造批记录与合规归档场景的部署
python·制造
k4m7v2pz41 分钟前
用 rust-verb-shell 重写进程管理:从 game.sh 到 .rvs 的迁移实录
开发语言·后端·rust