动态规划(DP)从入门到精通:原理详解与经典问题解析

动态规划(DP)从入门到精通:原理详解与经典问题解析

1. 引言

动态规划(Dynamic Programming,简称DP)是算法设计与优化中的一种重要方法,广泛应用于计算机科学、人工智能、运筹学等领域。许多经典问题,如背包问题、最短路径问题、编辑距离等,都可以通过动态规划高效解决。

本文将系统介绍动态规划的核心思想、解题步骤,并通过多个经典案例(附Java代码实现)帮助读者掌握动态规划的解题技巧。


2. 动态规划的核心思想

动态规划适用于具有以下两个性质的问题:

  1. 重叠子问题(Overlapping Subproblems)

    问题可以被分解为多个相似的子问题,且子问题会被重复计算。
    示例 :斐波那契数列 F(n) = F(n-1) + F(n-2),其中 F(3) 可能被 F(4)F(5) 多次计算。

  2. 最优子结构(Optimal Substructure)

    问题的最优解可以由其子问题的最优解推导而来。
    示例:最短路径问题中,A→C 的最短路径 = A→B 的最短路径 + B→C 的最短路径。

动态规划通过**记忆化存储(Memoization)填表法(Tabulation)**避免重复计算,提高效率。


3. 动态规划的解题步骤

步骤1:定义状态

  • 明确 dp[i]dp[i][j] 表示的含义。
  • 示例 :在斐波那契数列中,dp[i] 表示第 i 个斐波那契数。

步骤2:状态转移方程

  • 找出 dp[i]dp[i-1]dp[i-2] 等子问题的关系。
  • 示例dp[i] = dp[i-1] + dp[i-2](斐波那契数列)。

步骤3:初始化

  • 设置初始条件,如 dp[0]dp[1] 的值。
  • 示例dp[0] = 0, dp[1] = 1(斐波那契数列)。

步骤4:计算顺序

  • 确定填表顺序(通常自底向上)。
  • 示例 :计算 dp[2]dp[3] → ... → dp[n]

步骤5:返回结果

  • dp 表中提取最终答案。
  • 示例return dp[n](斐波那契数列)。

4. 经典动态规划问题

问题1:斐波那契数列(LeetCode 509)

问题描述 :计算第 n 个斐波那契数(F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2))。

Java实现
java 复制代码
public int fib(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];
}

// 空间优化版(O(1)空间)
public int fibOptimized(int n) {
    if (n <= 1) return n;
    int prev1 = 1, prev2 = 0;
    for (int i = 2; i <= n; i++) {
        int curr = prev1 + prev2;
        prev2 = prev1;
        prev1 = curr;
    }
    return prev1;
}

问题2:爬楼梯(LeetCode 70)

问题描述 :每次可以爬1或2阶台阶,求爬到第 n 阶有多少种方法。

状态转移方程

dp[i] = dp[i-1] + dp[i-2](类似斐波那契数列)

Java实现
java 复制代码
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 prev1 = 2, prev2 = 1;
    for (int i = 3; i <= n; i++) {
        int curr = prev1 + prev2;
        prev2 = prev1;
        prev1 = curr;
    }
    return prev1;
}

问题3:0-1背包问题

问题描述 :给定物品重量 weights 和价值 values,背包容量 W,求能装入的最大价值(每个物品只能选或不选)。

状态转移方程
css 复制代码
dp[i][w] = max(dp[i-1][w], values[i] + dp[i-1][w - weights[i]])
Java实现
java 复制代码
public int knapsack(int W, int[] weights, int[] values) {
    int n = weights.length;
    int[][] dp = new int[n + 1][W + 1];
    
    for (int i = 1; i <= n; i++) {
        for (int w = 1; w <= W; w++) {
            if (weights[i - 1] <= w) {
                dp[i][w] = Math.max(
                    dp[i - 1][w], 
                    values[i - 1] + dp[i - 1][w - weights[i - 1]]
                );
            } else {
                dp[i][w] = dp[i - 1][w];
            }
        }
    }
    return dp[n][W];
}

// 空间优化版(一维数组)
public int knapsackOptimized(int W, int[] weights, int[] values) {
    int[] dp = new int[W + 1];
    
    for (int i = 0; i < weights.length; i++) {
        for (int w = W; w >= weights[i]; w--) {
            dp[w] = Math.max(dp[w], values[i] + dp[w - weights[i]]);
        }
    }
    return dp[W];
}

5. 动态规划的优化技巧

  1. 空间优化

    • 很多二维DP可以优化为一维(如背包问题)。
  2. 状态压缩

    • 用位运算表示状态(如旅行商问题TSP)。
  3. 记忆化搜索(Memoization)

    • 自顶向下递归+缓存,避免重复计算。
  4. 滚动数组

    • 只保留必要的状态(如斐波那契数列只需 prev1prev2)。

6. 总结

动态规划是算法竞赛和面试中的高频考点,掌握其核心思想和经典问题解法至关重要。建议:

  1. 从简单问题入手(斐波那契、爬楼梯)。
  2. 理解状态转移方程的推导过程。
  3. 多练习LeetCode上的DP专题(如LIS、编辑距离、股票买卖问题)。
  4. 尝试优化空间复杂度。

希望本文能帮助你系统掌握动态规划!欢迎在评论区交流讨论。🚀

相关推荐
王中阳Go21 分钟前
从超市收银到航空调度:贪心算法如何破解生活中的最优决策谜题?
java·后端·算法
shepherd11122 分钟前
谈谈TransmittableThreadLocal实现原理和在日志收集记录系统上下文实战应用
java·后端·开源
维基框架23 分钟前
Spring Boot 项目整合Spring Security 进行身份验证
java·架构
故事挺秃然1 小时前
中文分词:机械分词算法详解与实践总结
算法·nlp
日月星辰Ace1 小时前
Java JVM 垃圾回收器(四):现代垃圾回收器 之 Shenandoah GC
java·jvm
天天摸鱼的java工程师2 小时前
商品详情页 QPS 达 10 万,如何设计缓存架构降低数据库压力?
java·后端·面试
天天摸鱼的java工程师2 小时前
设计一个分布式 ID 生成器,要求全局唯一、趋势递增、支持每秒 10 万次生成,如何实现?
java·后端·面试
阿杆2 小时前
一个看似普通的定时任务,如何优雅地毁掉整台服务器
java·后端·代码规范
粟悟饭&龟波功3 小时前
Java—— ArrayList 和 LinkedList 详解
java·开发语言
冷雨夜中漫步3 小时前
Java中如何使用lambda表达式分类groupby
java·开发语言·windows·llama