1动态规划入门:从斐波那契到网格路径

1. 动态规划基础概念

动态规划是一种将复杂问题分解为更小的子问题,并存储子问题的解以避免重复计算的方法。其核心思想包含三个关键特征:

  1. 最优子结构:一个问题的最优解包含其子问题的最优解
  2. 重叠子问题:子问题会被重复计算多次
  3. 状态转移方程:如何从子问题的解推导出原问题的解

2. 入门基础题目详解

2.1 斐波那契数 (Fibonacci Number)

问题描述:计算第n个斐波那契数,其中:

  • F(0) = 0, F(1) = 1
  • F(n) = F(n-1) + F(n-2), n ≥ 2
解法一:暴力递归(非DP)
python 复制代码
def fib_recursive(n):
    if n <= 1:
        return n
    return fib_recursive(n-1) + fib_recursive(n-2)

# 时间复杂度:O(2^n) - 指数级,存在大量重复计算
解法二:动态规划(自顶向下 - 记忆化搜索)
python 复制代码
def fib_memo(n, memo=None):
    if memo is None:
        memo = {}
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    
    memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
    return memo[n]
解法三:动态规划(自底向上 - 迭代)
python 复制代码
def fib_dp(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]
解法四:空间优化(滚动数组)
python 复制代码
def fib_optimized(n):
    if n <= 1:
        return n
    
    prev2, prev1 = 0, 1
    
    for i in range(2, n + 1):
        curr = prev1 + prev2
        prev2, prev1 = prev1, curr
    
    return prev1

Java实现

java 复制代码
public class Fibonacci {
    // 方法1:基础DP
    public int fibDP(int n) {
        if (n <= 1) return n;
        
        int[] dp = new int[n + 1];
        dp[0] = 0;
        dp[1] = 1;
        
        for (int i = 2; i <= n; i++) {
            dp[i] = dp[i-1] + dp[i-2];
        }
        
        return dp[n];
    }
    
    // 方法2:空间优化
    public int fibOptimized(int n) {
        if (n <= 1) return n;
        
        int prev2 = 0, prev1 = 1;
        
        for (int i = 2; i <= n; i++) {
            int curr = prev1 + prev2;
            prev2 = prev1;
            prev1 = curr;
        }
        
        return prev1;
    }
}

2.2 爬楼梯 (Climbing Stairs)

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

状态定义

dp[i]:爬到第i阶楼梯的方法数

状态转移方程
复制代码
dp[i] = dp[i-1] + dp[i-2]
  • 从第i-1阶爬1步
  • 从第i-2阶爬2步
Python实现
python 复制代码
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_optimized(n):
    if n <= 2:
        return n
    
    prev2, prev1 = 1, 2
    
    for i in range(3, n + 1):
        curr = prev1 + prev2
        prev2, prev1 = prev1, curr
    
    return prev1
Java实现
java 复制代码
public class ClimbingStairs {
    public int climbStairs(int n) {
        if (n <= 2) return n;
        
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 2;
        
        for (int i = 3; i <= n; i++) {
            dp[i] = dp[i-1] + dp[i-2];
        }
        
        return dp[n];
    }
    
    public int climbStairsOptimized(int n) {
        if (n <= 2) return n;
        
        int prev2 = 1, prev1 = 2;
        
        for (int i = 3; i <= n; i++) {
            int curr = prev1 + prev2;
            prev2 = prev1;
            prev1 = curr;
        }
        
        return prev1;
    }
}

2.3 使用最小花费爬楼梯 (Min Cost Climbing Stairs)

问题描述:每个台阶有一个cost[i],支付cost[i]后可以爬1或2步,求爬到顶部的最小花费。

状态定义

dp[i]:到达第i阶的最小花费

状态转移方程
复制代码
dp[i] = min(dp[i-1], dp[i-2]) + cost[i]

注意:最后要到达顶部(第n阶之后)

Python实现
python 复制代码
def minCostClimbingStairs(cost):
    n = len(cost)
    if n <= 1:
        return 0
    
    dp = [0] * n
    dp[0] = cost[0]
    dp[1] = cost[1]
    
    for i in range(2, n):
        dp[i] = min(dp[i-1], dp[i-2]) + cost[i]
    
    # 到达顶部可以从倒数第一或倒数第二步直接到达
    return min(dp[n-1], dp[n-2])

# 空间优化版本
def minCostClimbingStairs_optimized(cost):
    n = len(cost)
    if n <= 1:
        return 0
    
    prev2, prev1 = cost[0], cost[1]
    
    for i in range(2, n):
        curr = min(prev1, prev2) + cost[i]
        prev2, prev1 = prev1, curr
    
    return min(prev1, prev2)
Java实现
java 复制代码
public class MinCostClimbingStairs {
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        if (n <= 1) return 0;
        
        int[] dp = new int[n];
        dp[0] = cost[0];
        dp[1] = cost[1];
        
        for (int i = 2; i < n; i++) {
            dp[i] = Math.min(dp[i-1], dp[i-2]) + cost[i];
        }
        
        return Math.min(dp[n-1], dp[n-2]);
    }
    
    public int minCostClimbingStairsOptimized(int[] cost) {
        int n = cost.length;
        if (n <= 1) return 0;
        
        int prev2 = cost[0];
        int prev1 = cost[1];
        
        for (int i = 2; i < n; i++) {
            int curr = Math.min(prev1, prev2) + cost[i];
            prev2 = prev1;
            prev1 = curr;
        }
        
        return Math.min(prev1, prev2);
    }
}

2.4 不同路径 (Unique Paths)

问题描述:m×n网格,从左上角到右下角,每次只能向右或向下移动,求不同路径数。

状态定义

dp[i][j]:到达(i, j)位置的不同路径数

状态转移方程
复制代码
dp[i][j] = dp[i-1][j] + dp[i][j-1]
  • 从上方下来:dp[i-1][j]
  • 从左方过来:dp[i][j-1]
Python实现
python 复制代码
def uniquePaths(m, n):
    # 创建DP数组
    dp = [[0] * n for _ in range(m)]
    
    # 初始化第一行和第一列
    for i in range(m):
        dp[i][0] = 1
    for j in range(n):
        dp[0][j] = 1
    
    # 填充DP表
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = dp[i-1][j] + dp[i][j-1]
    
    return dp[m-1][n-1]

# 空间优化版本(使用一维数组)
def uniquePaths_optimized(m, n):
    dp = [1] * n
    
    for i in range(1, m):
        for j in range(1, n):
            dp[j] += dp[j-1]
    
    return dp[n-1]
Java实现
java 复制代码
public class UniquePaths {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        
        // 初始化第一列
        for (int i = 0; i < m; i++) {
            dp[i][0] = 1;
        }
        
        // 初始化第一行
        for (int j = 0; j < n; j++) {
            dp[0][j] = 1;
        }
        
        // 填充DP表
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        
        return dp[m-1][n-1];
    }
    
    // 空间优化版本
    public int uniquePathsOptimized(int m, int n) {
        int[] dp = new int[n];
        
        // 初始化第一行
        for (int j = 0; j < n; j++) {
            dp[j] = 1;
        }
        
        // 从第二行开始计算
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[j] += dp[j-1];
            }
        }
        
        return dp[n-1];
    }
}

2.5 不同路径 II (Unique Paths with Obstacles)

问题描述:带障碍物的网格,障碍物处不能通过。

状态转移方程的修改
复制代码
if obstacleGrid[i][j] == 1:
    dp[i][j] = 0
else:
    dp[i][j] = dp[i-1][j] + dp[i][j-1]
Python实现
python 复制代码
def uniquePathsWithObstacles(obstacleGrid):
    m, n = len(obstacleGrid), len(obstacleGrid[0])
    
    # 如果起点或终点有障碍物
    if obstacleGrid[0][0] == 1 or obstacleGrid[m-1][n-1] == 1:
        return 0
    
    dp = [[0] * n for _ in range(m)]
    dp[0][0] = 1
    
    # 初始化第一列
    for i in range(1, m):
        if obstacleGrid[i][0] == 0:
            dp[i][0] = dp[i-1][0]
    
    # 初始化第一行
    for j in range(1, n):
        if obstacleGrid[0][j] == 0:
            dp[0][j] = dp[0][j-1]
    
    # 填充DP表
    for i in range(1, m):
        for j in range(1, n):
            if obstacleGrid[i][j] == 0:
                dp[i][j] = dp[i-1][j] + dp[i][j-1]
            else:
                dp[i][j] = 0
    
    return dp[m-1][n-1]

# 空间优化版本
def uniquePathsWithObstacles_optimized(obstacleGrid):
    m, n = len(obstacleGrid), len(obstacleGrid[0])
    
    if obstacleGrid[0][0] == 1:
        return 0
    
    dp = [0] * n
    dp[0] = 1
    
    for i in range(m):
        for j in range(n):
            if obstacleGrid[i][j] == 1:
                dp[j] = 0
            elif j > 0:
                dp[j] += dp[j-1]
    
    return dp[n-1]
Java实现
java 复制代码
public class UniquePathsWithObstacles {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        
        // 起点或终点有障碍物
        if (obstacleGrid[0][0] == 1 || obstacleGrid[m-1][n-1] == 1) {
            return 0;
        }
        
        int[][] dp = new int[m][n];
        dp[0][0] = 1;
        
        // 初始化第一列
        for (int i = 1; i < m; i++) {
            if (obstacleGrid[i][0] == 0) {
                dp[i][0] = dp[i-1][0];
            }
        }
        
        // 初始化第一行
        for (int j = 1; j < n; j++) {
            if (obstacleGrid[0][j] == 0) {
                dp[0][j] = dp[0][j-1];
            }
        }
        
        // 填充DP表
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (obstacleGrid[i][j] == 0) {
                    dp[i][j] = dp[i-1][j] + dp[i][j-1];
                } else {
                    dp[i][j] = 0;
                }
            }
        }
        
        return dp[m-1][n-1];
    }
    
    // 空间优化版本
    public int uniquePathsWithObstaclesOptimized(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        
        if (obstacleGrid[0][0] == 1) {
            return 0;
        }
        
        int[] dp = new int[n];
        dp[0] = 1;
        
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (obstacleGrid[i][j] == 1) {
                    dp[j] = 0;
                } else if (j > 0) {
                    dp[j] += dp[j-1];
                }
            }
        }
        
        return dp[n-1];
    }
}

3. 动态规划解题模板总结

3.1 解题步骤

  1. 定义状态:确定dp数组的含义
  2. 确定初始条件:基础情况的值
  3. 写出状态转移方程:如何从已知状态推导新状态
  4. 确定遍历顺序:确保计算当前状态时依赖的状态已计算
  5. 确定返回值:dp数组的哪个元素是最终答案

3.2 空间优化技巧

  • 滚动数组:当状态只依赖前几个状态时
  • 降维:二维DP可优化为一维(如不同路径问题)
  • 压缩状态:使用位运算等技巧

3.3 常见错误

  1. 忘记初始化:dp[0]、dp[1]等初始值
  2. 数组越界:访问dp[i-1]时i=0
  3. 遍历顺序错误:如背包问题的内外层循环
  4. 数据类型溢出:使用long代替int

4. 练习建议

4.1 顺序练习

  1. 先掌握基础的五道题
  2. 理解状态定义和转移方程的区别
  3. 练习空间优化版本
  4. 尝试自己推导状态转移方程

4.2 对比分析

  • 比较斐波那契和爬楼梯的相似性
  • 对比不同路径有无障碍物的区别
  • 分析最小花费爬楼梯与普通爬楼梯的差异

4.3 扩展思考

  1. 如果爬楼梯可以爬1、2、3步怎么办?
  2. 不同路径问题如果允许斜向移动?
  3. 如果每个格子的移动有不同代价?

5. 复杂度分析

题目 时间复杂度 空间复杂度 优化后空间复杂度
斐波那契数 O(n) O(n) O(1)
爬楼梯 O(n) O(n) O(1)
最小花费爬楼梯 O(n) O(n) O(1)
不同路径 O(m×n) O(m×n) O(n)
不同路径II O(m×n) O(m×n) O(n)

6. 总结

入门基础的动态规划问题主要训练以下几个能力:

  1. 识别子问题:找到问题的最优子结构
  2. 定义状态:选择合适的dp数组维度
  3. 推导方程:建立状态间的数学关系
  4. 优化空间:理解如何减少空间复杂度

掌握这五道题后,你已经建立了动态规划的基本思维框架。接下来可以挑战更复杂的背包问题、序列DP等进阶题目。

关键点:动态规划的核心是"记住已经求过的解",避免重复计算。从最简单的斐波那契开始理解这个思想,逐步应用到更复杂的问题中。

相关推荐
zhangfeng11332 小时前
大语言模型 bpe算法 后面对接的是 one-hot吗 nn.Embedding
算法·语言模型·embedding
Pluchon2 小时前
硅基计划4.0 算法 动态规划高阶
java·数据结构·算法·leetcode·深度优先·动态规划
科学计算技术爱好者3 小时前
NVIDIA GPU 系列用途分类梳理
人工智能·算法·gpu算力
程序员敲代码吗3 小时前
嵌入式C++开发注意事项
开发语言·c++·算法
好学且牛逼的马3 小时前
【Hot100|14-LeetCode53. 最大子数组和】
数据结构·算法·leetcode
无心水3 小时前
17、Go协程通关秘籍:主协程等待+多协程顺序执行实战解析
开发语言·前端·后端·算法·golang·go·2025博客之星评选投票
东华果汁哥3 小时前
【机器视觉 行人检测算法】FastAPI 部署 YOLO 11行人检测 API 服务教程
算法·yolo·fastapi
每天学一点儿3 小时前
[SimpleITK] 教程 63:配准初始化 (Registration Initialization) —— 从几何对齐到手动干预。
算法
君义_noip3 小时前
信息学奥赛一本通 1463:门票
c++·算法·哈希算法·信息学奥赛·csp-s