🧩 动态规划入门 — 爬楼梯与背包问题初探

从暴力递归到记忆化搜索再到DP表——动态规划是算法面试的半壁江山

📖 动态规划的本质

动态规划(Dynamic Programming, DP)是将复杂问题拆解为重叠子问题,通过记忆化避免重复计算的算法范式。DP是算法面试中最重要的主题——在LeetCode中DP题目超过500道,是出题频率最高的算法类型。

动态规划解题框架: ┌─────────────────────────────────────────┐ │ DP 解题六步法 │ ├─────────────────────────────────────────┤ │ 1. 定义状态: dp[i] 代表什么? │ │ --> 最关键的步骤,决定整个解法 │ │ 2. 状态转移: dp[i] = f(dp[i-1], ...) │ │ --> 核心方程,需要推导和验证 │ │ 3. 初始条件: dp[0] = ?, dp[1] = ? │ │ --> 边界情况,容易遗漏 │ │ 4. 遍历顺序: 从小到大 or 从大到小? │ │ --> 依赖关系决定方向 │ │ 5. 返回结果: dp[n] or max(dp)? │ │ --> 根据问题确定 │ │ 6. 空间优化: 能否滚动数组? │ │ --> 面试加分项 │ └─────────────────────────────────────────┘ DP vs 递归 vs 贪心: 递归: 自顶向下,大量重复计算 O(2^n) 记忆化: 自顶向下,缓存避免重复 O(n) DP: 自底向上,填表法,最常用 O(n) 贪心: 不回头,每步最优(不一定对) O(n)

1. DP的两个核心性质

最优子结构:原问题的最优解包含子问题的最优解。即:大问题的最优解可以由小问题的最优解推导而来。

重叠子问题:不同的递归路径会访问相同的子问题。这正是DP的价值——用空间换时间,每个子问题只算一次。

2. 从递归到DP的演进——以斐波那契为例

# 暴力递归 O(2^n) — 大量重复计算
def fib(n):
    if n <= 1: return n
    return fib(n-1) + fib(n-2)

# 记忆化搜索 O(n) — 自顶向下 + 缓存
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
    if n <= 1: return n
    return fib(n-1) + fib(n-2)

# 动态规划 O(n) — 自底向上填表
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(log n) — 进阶
def fib(n):
    if n <= 1: return n
    def mul(A, B):
        return [[A[0][0]*B[0][0]+A[0][1]*B[1][0],
                 A[0][0]*B[0][1]+A[0][1]*B[1][1]],
                [A[1][0]*B[0][0]+A[1][1]*B[1][0],
                 A[1][0]*B[0][1]+A[1][1]*B[1][1]]]
    def pow_m(M, n):
        if n == 1: return M
        h = pow_m(M, n//2)
        return mul(mul(h,h), M) if n%2 else mul(h,h)
    return pow_m([[1,1],[1,0]], n)[0][1]

3. DP题目分类概览

类型代表题目难度
线性DP爬楼梯、打家劫舍入门
背包DP01背包、完全背包中等
子序列DPLCS、LIS中等
路径DP最小路径和、不同路径入门
字符串DP编辑距离、正则匹配较难
树形DP打家劫舍III中等
状态压缩DP旅行商、棋盘覆盖困难

🔬 题目一:爬楼梯(LC 70)

每次可以爬1或2个台阶,爬到第n阶有多少种不同方法?

思路分析

爬楼梯状态分析: n=1: 1种 (1) n=2: 2种 (1+1, 2) n=3: 3种 (1+1+1, 1+2, 2+1) n=4: 5种 (1+1+1+1, 1+1+2, 1+2+1, 2+1+1, 2+2) n=5: 8种 ... 规律: dp[n] = dp[n-1] + dp[n-2] 到第n阶的方法 = 从n-1阶走1步 + 从n-2阶走2步 本质: 斐波那契数列! 为什么?到第n阶的最后一步只可能是: - 从n-1阶走1步上来 - 从n-2阶走2步上来 两种情况互斥且完备,加法原理!
DP分析:
1. 状态:dp[i] = 爬到第i阶的方法数
2. 转移:dp[i] = dp[i-1] + dp[i-2]
3. 初始:dp[1] = 1, dp[2] = 2
4. 结果:dp[n]
5. 空间优化:只需前两个状态,滚动变量

代码实现

class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 2:
            return n
        a, b = 1, 2
        for _ in range(3, n + 1):
            a, b = b, a + b
        return b

# 完整DP版本(便于理解)
class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 2:
            return n
        dp = [0] * (n + 1)
        dp[1], dp[2] = 1, 2
        for i in range(3, n + 1):
            dp[i] = dp[i-1] + dp[i-2]
        return dp[n]

复杂度分析

时间: O(n) 空间: O(1)(滚动数组优化后)

爬楼梯是DP最基础的模型。变体很多:可以走1/2/3步(加dp[i-3]项)、走k步(前k项之和)、有些台阶不能走(dp[i]=0)等。核心都是定义好"从哪来"的状态转移。

🔬 题目二:使用最小花费爬楼梯(LC 746)

给定数组 cost,cost[i]是从第i个台阶向上爬的花费,可以从下标0或1开始,求到达顶部的最小花费。

思路分析

最小花费爬楼梯: cost = [10, 15, 20] 台阶: 0 1 2 [顶部] 10 15 20 dp[i] = 到达第i个台阶的最小花费 dp[0] = 0 (可以从0出发, 还没花钱) dp[1] = 0 (可以从1出发, 还没花钱) dp[2] = min(dp[0]+cost[0], dp[1]+cost[1]) = min(10, 15) = 10 dp[3] = min(dp[1]+cost[1], dp[2]+cost[2]) = min(15, 30) = 15 答案 = dp[3] = 15 关键理解: cost[i]是从i出发向上跳的花费 不是"到达i的花费",而是"从i离开的花费"
DP分析:
1. 状态:dp[i] = 到达第i个台阶的最小花费
2. 转移:dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])
3. 初始:dp[0] = 0, dp[1] = 0(起点不花钱)
4. 结果:dp[n](n = len(cost),到达顶部)

代码实现

class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        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]

# 空间优化版本
class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        a, b = 0, 0
        for c in cost:
            a, b = b, min(a, b) + c
        return min(a, b)

复杂度分析

时间: O(n) 空间: O(n),可优化到O(1)

🔬 题目三:打家劫舍(LC 198)

给定非负整数数组,代表各房屋金额,不能偷相邻房屋,求能偷到的最大金额。

思路分析

打家劫舍 -- 选/不选分叉: nums = [2, 7, 9, 3, 1] dp[i] = 前i个房屋能偷到的最大金额 对每个房屋, 有两个选择: 1. 不偷i号: dp[i] = dp[i-1] (金额不变) 2. 偷i号: dp[i] = dp[i-2] + nums[i] (不能偷i-1) dp[i] = max(dp[i-1], dp[i-2] + nums[i]) i=0: dp=2 (偷0号) i=1: dp=7 (偷1号, 7>2) i=2: dp=11 (偷0+2号 = 2+9 = 11 > 7) i=3: dp=11 (偷1+3号 = 7+3 = 10 < 11, 不偷3号) i=4: dp=12 (偷0+2+4号 = 2+9+1 = 12 > 11) 这个"选/不选"模式是DP的核心! 后面树形DP(打家劫舍III)也用同样的思想

代码实现

class Solution:
    def rob(self, nums: List[int]) -> int:
        if not nums:
            return 0
        if len(nums) <= 2:
            return max(nums)
        prev2, prev1 = nums[0], max(nums[0], nums[1])
        for i in range(2, len(nums)):
            prev2, prev1 = prev1, max(prev1, prev2 + nums[i])
        return prev1

# 带dp数组版本(更清晰)
class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 0: return 0
        if n == 1: return nums[0]
        dp = [0] * n
        dp[0] = nums[0]
        dp[1] = max(nums[0], nums[1])
        for i in range(2, n):
            dp[i] = max(dp[i-1], dp[i-2] + nums[i])
        return dp[n-1]

复杂度分析

时间: O(n) 空间: O(1)

打家劫舍是DP经典入门题。它的转移方程模式——dp[i] = max(不选, 选)——在后续的背包问题、树形DP(LC 337)中反复出现。理解"选/不选"的分叉思想是掌握DP的关键。这是所有DP问题的思维原型。

面试实战技巧

在实际面试中,算法题不仅仅是写代码,更考察沟通、分析和解决问题的能力。以下是一些实战技巧:

1. 解题步骤模板

5分钟分析:理解题意 - 确认边界 - 想暴力解 - 优化 - 写代码

1. 先复述题目,确认理解无误
2. 问清边界条件:空输入?负数?溢出?
3. 先说暴力解法(证明你理解问题)
4. 分析瓶颈,提出优化思路
5. 边写代码边解释思路

2. 常见陷阱

通用陷阱:
1. 索引越界:特别注意循环范围和数组边界
2. 初始化遗漏:DP的初始条件是否正确?
3. 遍历方向:依赖关系决定遍历顺序
4. 整数溢出:Python不溢出但其他语言注意
5. 边界条件:空数组、单元素、全相同

3. 代码模板

# 通用解题模板
class Solution:
    def solve(self, input_data):
        # 1. 边界检查
        if not input_data:
            return default_value
        # 2. 初始化
        result = initial_value
        state = initial_state
        # 3. 主循环
        for item in input_data:
            state = transition(state, item)
            result = max/min/update(result, state)
        # 4. 返回结果
        return result
面试中的代码不是越短越好,而是越清晰越好。适当的注释、合理的变量名、分步骤的实现,比一行式解法更能展示工程素养。
成就解锁:DP入门者 — 掌握动态规划三大模型:爬楼梯、最小花费、打家劫舍
LeetCode AC验证:LC 70 Climbing Stairs ✅ | LC 746 Min Cost Climbing Stairs ✅ | LC 198 House Robber ✅

📝 课后练习

  1. LC 509 斐波那契数
  2. LC 1137 第N个泰波那契数
  3. LC 213 打家劫舍II
  4. LC 740 删除并获得点数
  5. LC 91 解码方法

🔑 本课要点回顾

📚 扩展阅读

思考题:爬楼梯问题中,如果可以走1步、2步或3步,状态转移方程是什么?