coding-interview-university动态规划专题:从入门到精通

coding-interview-university动态规划专题:从入门到精通

【免费下载链接】coding-interview-university 一份完整的计算机科学学习计划,以成为软件工程师为目标 【免费下载链接】coding-interview-university 项目地址: https://gitcode.com/GitHub_Trending/co/coding-interview-university

你是否在面对动态规划(Dynamic Programming, DP)问题时感到无从下手?是否觉得递归解法效率低下却找不到优化方向?本文将系统拆解动态规划的核心原理、解题框架与实战技巧,带你从理论到实践全面掌握这一面试必备技能。读完本文,你将能够:

  • 快速识别动态规划适用场景
  • 熟练运用状态定义与转移方程设计技巧
  • 掌握空间优化的常用策略
  • 解决90%以上的经典DP面试题

动态规划本质:从暴力递归到智能递推

1.1 动态规划的核心思想

动态规划是一种通过将复杂问题分解为重叠子问题,并存储子问题解(即记忆化)来避免重复计算的优化技术。其本质是用空间换时间,将指数级时间复杂度优化为多项式级别。

mermaid

1.2 动态规划三要素

要素定义作用
状态定义描述问题在某一阶段的具体形态建立问题与子问题的映射关系
转移方程子问题之间的递推关系实现从已知解推未知解的过程
边界条件最小子问题的直接解避免无限递归,确定递推起点

示例:斐波那契数列

  • 状态定义:dp[i] 表示第i个斐波那契数
  • 转移方程:dp[i] = dp[i-1] + dp[i-2]
  • 边界条件:dp[0] = 0, dp[1] = 1

动态规划解题四步法

2.1 步骤拆解

步骤1:问题建模与状态定义

状态定义是动态规划的核心,需要回答两个问题:

  • 用哪些变量描述问题状态?
  • 状态变量的取值范围是什么?

实战技巧

  • 从问题最后一步倒推
  • 考虑是否需要多维状态(如二维DP数组)
  • 状态维度尽可能精简
步骤2:转移方程推导

转移方程是状态之间的递推关系,常见推导方法:

  • 分类讨论法(如0-1背包中的选与不选)
  • 归纳法(从简单情况推广到一般情况)
  • 状态压缩法(降维优化)

示例:最长递增子序列(LIS)

# 状态定义:dp[i]表示以nums[i]结尾的LIS长度
# 转移方程:dp[i] = max(dp[j] + 1) for j < i and nums[j] < nums[i]
def lengthOfLIS(nums):
    if not nums: return 0
    dp = [1] * len(nums)
    for i in range(len(nums)):
        for j in range(i):
            if nums[j] < nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp)
步骤3:边界条件处理

边界条件直接影响解的正确性,需注意:

  • 初始状态的赋值
  • 特殊情况的处理(如空输入)
  • 数组越界问题
步骤4:空间优化

常见优化策略:

  • 滚动数组(将二维降为一维)
  • 变量替换(用单个变量代替数组)
  • 状态压缩(如0-1背包的逆序遍历)

示例:0-1背包空间优化

# 优化前:二维数组
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])

# 优化后:一维数组(逆序遍历避免覆盖)
for i in range(n):
    for j in range(capacity, weight[i]-1, -1):
        dp[j] = max(dp[j], dp[j-weight[i]] + value[i])

经典动态规划问题分类解析

3.1 线性DP

特点:状态按线性顺序递推,是最基础的DP类型。

3.1.1 爬楼梯问题
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.1.2 最大子数组和(Kadane算法)
def maxSubArray(nums):
    dp = [0] * len(nums)
    dp[0] = nums[0]
    max_sum = dp[0]
    for i in range(1, len(nums)):
        dp[i] = max(nums[i], dp[i-1] + nums[i])
        max_sum = max(max_sum, dp[i])
    return max_sum

3.2 区间DP

特点:状态定义在区间上,通常用二维数组dp[i][j]表示区间[i,j]的最优解。

3.2.1 最长回文子序列
def longestPalindromeSubseq(s):
    n = len(s)
    dp = [[0]*n for _ in range(n)]
    
    # 边界条件:长度为1的区间
    for i in range(n-1, -1, -1):
        dp[i][i] = 1
        # 填充长度>1的区间
        for j in range(i+1, n):
            if s[i] == s[j]:
                dp[i][j] = dp[i+1][j-1] + 2
            else:
                dp[i][j] = max(dp[i+1][j], dp[i][j-1])
    return dp[0][n-1]

3.3 背包问题

3.3.1 完全背包(物品可重复选择)
def coinChange(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for coin in coins:
        for j in range(coin, amount + 1):
            dp[j] = min(dp[j], dp[j - coin] + 1)
    return dp[amount] if dp[amount] != float('inf') else -1

3.4 状态压缩DP

3.4.1 打家劫舍问题
def rob(nums):
    if not nums: return 0
    n = len(nums)
    if n == 1: return nums[0]
    
    # dp[i] = max(dp[i-1], dp[i-2] + nums[i])
    prev, curr = nums[0], max(nums[0], nums[1])
    for i in range(2, n):
        prev, curr = curr, max(curr, prev + nums[i])
    return curr

动态规划优化技巧进阶

4.1 时间复杂度优化

4.1.1 单调队列优化

在某些问题中(如滑动窗口最大值),可通过维护单调队列将O(n^2)优化为O(n)。

4.1.2 矩阵快速幂

用于求解线性递推关系,将O(n)优化为O(log n)。以斐波那契数列为例:

def fib(n):
    if n <= 1: return n
    # 矩阵乘法
    def multiply(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 matrix_pow(mat, power):
        result = [[1,0],[0,1]]  # 单位矩阵
        while power > 0:
            if power % 2 == 1:
                result = multiply(result, mat)
            mat = multiply(mat, mat)
            power //= 2
        return result
    mat = [[1,1],[1,0]]
    return matrix_pow(mat, n-1)[0][0]

4.2 空间优化策略对比

优化方法适用场景时间复杂度影响空间复杂度优化
滚动数组状态仅依赖前几行不变O(n)→O(1)
变量替换状态仅依赖前一个值不变O(n)→O(1)
逆序遍历0-1背包问题不变O(nm)→O(m)

动态规划面试解题模板

5.1 解题流程图

mermaid

5.2 状态设计 checklist

  •  是否包含所有必要信息?
  •  是否可以进一步精简?
  •  维度是否最低?
  •  是否容易推导转移方程?

5.3 常见错误排查

  1. 状态定义不准确:导致子问题覆盖不全
  2. 转移方程遗漏情况:如边界条件处理不当
  3. 数组越界:循环范围错误
  4. 空间优化错误:覆盖了尚未使用的子问题解
  5. 初始化错误:未正确设置初始状态

实战训练:高频面试题精解

6.1 股票买卖问题(最多交易两次)

def maxProfit(prices):
    if not prices: return 0
    n = len(prices)
    # 定义4种状态:
    # buy1, sell1, buy2, sell2
    buy1 = buy2 = -prices[0]
    sell1 = sell2 = 0
    
    for price in prices[1:]:
        buy1 = max(buy1, -price)
        sell1 = max(sell1, buy1 + price)
        buy2 = max(buy2, sell1 - price)
        sell2 = max(sell2, buy2 + price)
    
    return max(sell1, sell2)

6.2 编辑距离问题

def minDistance(word1, word2):
    m, n = len(word1), len(word2)
    # dp[i][j]表示word1[:i]到word2[:j]的最小编辑距离
    dp = [[0]*(n+1) for _ in range(m+1)]
    
    # 边界条件
    for i in range(m+1):
        dp[i][0] = i
    for j in range(n+1):
        dp[0][j] = j
    
    # 填充dp表
    for i in range(1, m+1):
        for j in range(1, n+1):
            if word1[i-1] == word2[j-1]:
                dp[i][j] = dp[i-1][j-1]
            else:
                dp[i][j] = min(
                    dp[i-1][j] + 1,    # 删除
                    dp[i][j-1] + 1,    # 插入
                    dp[i-1][j-1] + 1   # 替换
                )
    return dp[m][n]

总结与展望

动态规划作为算法面试中的重点和难点,需要通过大量练习培养问题拆解能力和状态设计直觉。掌握本文介绍的解题框架和优化技巧后,建议通过以下步骤进一步提升:

  1. 分类刷题:按线性DP、区间DP、背包问题等类别集中训练
  2. 一题多解:尝试不同的状态定义和解法优化
  3. 总结归纳:建立个人的DP问题模型库
  4. 模拟面试:限时解题,训练快速反应能力

动态规划不仅是面试必备技能,更是培养算法思维的重要途径。掌握动态规划后,你将能够以更高效的方式解决复杂问题,为未来的职业发展打下坚实基础。

练习资源推荐

  • LeetCode动态规划标签(中等难度20题+困难难度10题)
  • 《算法导论》第15章动态规划
  • 《Cracking the Coding Interview》第8章递归与动态规划

【免费下载链接】coding-interview-university 一份完整的计算机科学学习计划,以成为软件工程师为目标 【免费下载链接】coding-interview-university 项目地址: https://gitcode.com/GitHub_Trending/co/coding-interview-university

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值