动态规划详解(二):从暴力递归到动态规划的完整优化之路

目录

[一、什么是动态规划?------ 从人类直觉到算法思维](#一、什么是动态规划?—— 从人类直觉到算法思维)

二、暴力递归:最直观的问题分解方式

[1. 示例:斐波那契数列](#1. 示例:斐波那契数列)

[2. 递归树分析(以n=5为例)](#2. 递归树分析(以n=5为例))

[3. 问题暴露](#3. 问题暴露)

三、第一次优化:记忆化搜索(Memoization)

[1. 核心思想](#1. 核心思想)

[2. 斐波那契优化实现](#2. 斐波那契优化实现)

[3. 复杂度分析](#3. 复杂度分析)

四、第二次进化:自底向上动态规划

[1. 思维转变](#1. 思维转变)

[2. 斐波那契DP实现](#2. 斐波那契DP实现)

[3. 空间优化(滚动数组)](#3. 空间优化(滚动数组))

[五、经典案例:爬楼梯问题(LeetCode 70)](#五、经典案例:爬楼梯问题(LeetCode 70))

[1. 问题描述](#1. 问题描述)

[2. 暴力递归解法](#2. 暴力递归解法)

[3. DP优化实现](#3. DP优化实现)

[4. 状态转移方程推导](#4. 状态转移方程推导)

六、高阶案例:0-1背包问题

[1. 问题描述](#1. 问题描述)

[2. 暴力递归解法](#2. 暴力递归解法)

[3. 记忆化搜索优化](#3. 记忆化搜索优化)

[4. 动态规划终极形态](#4. 动态规划终极形态)

[5. 空间压缩技巧(滚动数组)](#5. 空间压缩技巧(滚动数组))

七、动态规划解题方法论总结

[1. 五步法流程](#1. 五步法流程)

[2. 优化路线图](#2. 优化路线图)

[3. 常见问题处理技巧](#3. 常见问题处理技巧)

八、实战练习建议


一、什么是动态规划?------ 从人类直觉到算法思维

动态规划(Dynamic Programming, DP) 本质是一种通过**"聪明的穷举"解决问题的思想。它的核心是发现重叠子问题和最优子结构**,并通过存储中间结果避免重复计算。我们可以通过一个认知升级路线来理解它:

javascript 复制代码
暴力递归 → 发现重复计算 → 记忆化搜索 → 推导状态转移 → 自底向上动态规划

二、暴力递归:最直观的问题分解方式

1. 示例:斐波那契数列

java 复制代码
// 经典递归实现
public int fib(int n) {
    if (n <= 1) return n;
    return fib(n-1) + fib(n-2);
}
复制代码

2. 递归树分析(以n=5为例)

javascript 复制代码
     fib(5)
    /   \
  fib(4)   fib(3)
  /  \   /  \
fib(3) fib(2) fib(2) fib(1)
...(继续展开)...
复制代码

3. 问题暴露

  • 重复计算:fib(3)计算2次,fib(2)计算3次

  • 指数级复杂度:O(2^n) 时间复杂度,O(n) 栈空间


三、第一次优化:记忆化搜索(Memoization)

1. 核心思想

  • 空间换时间:使用数组/HashMap存储已计算结果

  • 剪枝优化:计算前先查询存储结构

2. 斐波那契优化实现

java 复制代码
public int fibMemo(int n) {
    int[] memo = new int[n+1];
    Arrays.fill(memo, -1);
    return dfs(n, memo);
}

private int dfs(int n, int[] memo) {
    if (n <= 1) return n;
    if (memo[n] != -1) return memo[n];
    memo[n] = dfs(n-1, memo) + dfs(n-2, memo);
    return memo[n];
}
复制代码

3. 复杂度分析

  • 时间复杂度:O(n) ------ 每个子问题只计算一次

  • 空间复杂度:O(n) 递归栈 + O(n) 存储空间


四、第二次进化:自底向上动态规划

1. 思维转变

java 复制代码
递归(自顶向下) → 迭代(自底向上)

2. 斐波那契DP实现

java 复制代码
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];
}
复制代码

3. 空间优化(滚动数组)

java 复制代码
public int fibOpt(int n) {
    if (n <= 1) return n;
    int prev = 0, curr = 1;
    for (int i = 2; i <= n; i++) {
        int sum = prev + curr;
        prev = curr;
        curr = sum;
    }
    return curr;
}
复制代码

五、经典案例:爬楼梯问题(LeetCode 70)

1. 问题描述

每次可以爬1或2个台阶,求到达第n阶的不同方法数

2. 暴力递归解法

java 复制代码
public int climbStairs(int n) {
    if (n == 1) return 1;
    if (n == 2) return 2;
    return climbStairs(n-1) + climbStairs(n-2);
}
复制代码

3. DP优化实现

java 复制代码
public int climbStairsDP(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];
}
复制代码

4. 状态转移方程推导

java 复制代码
dp[i] = dp[i-1] + dp[i-2]
解释:到达第i阶的方法数 = 从i-1阶上1步 + 从i-2阶上2步
复制代码

六、高阶案例:0-1背包问题

1. 问题描述

给定物品重量w[]和价值v[],背包容量C,求最大价值

2. 暴力递归解法

java 复制代码
public int knapsack(int[] w, int[] v, int C) {
    return dfs(w, v, w.length-1, C);
}

private int dfs(int[] w, int[] v, int index, int cap) {
    if (index < 0 || cap <= 0) return 0;
    
    // 不选当前物品
    int no = dfs(w, v, index-1, cap);
    
    // 选当前物品(前提:容量足够)
    int yes = cap >= w[index] ? 
        dfs(w, v, index-1, cap - w[index]) + v[index] : 0;
        
    return Math.max(no, yes);
}
复制代码

3. 记忆化搜索优化

java 复制代码
public int knapsackMemo(int[] w, int[] v, int C) {
    int n = w.length;
    int[][] memo = new int[n][C+1];
    return dfs(w, v, n-1, C, memo);
}

private int dfs(int[] w, int[] v, int index, int cap, int[][] memo) {
    if (index < 0 || cap <= 0) return 0;
    if (memo[index][cap] != 0) return memo[index][cap];
    
    int no = dfs(w, v, index-1, cap, memo);
    int yes = cap >= w[index] ? 
        dfs(w, v, index-1, cap - w[index], memo) + v[index] : 0;
    
    memo[index][cap] = Math.max(no, yes);
    return memo[index][cap];
}
复制代码

4. 动态规划终极形态

java 复制代码
public int knapsackDP(int[] w, int[] v, int C) {
    int n = w.length;
    int[][] dp = new int[n+1][C+1];
    
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= C; j++) {
            if (j < w[i-1]) {
                dp[i][j] = dp[i-1][j]; // 装不下
            } else {
                dp[i][j] = Math.max(
                    dp[i-1][j], 
                    dp[i-1][j - w[i-1]] + v[i-1]
                );
            }
        }
    }
    return dp[n][C];
}
复制代码

5. 空间压缩技巧(滚动数组)

java 复制代码
public int knapsackOpt(int[] w, int[] v, int C) {
    int[] dp = new int[C+1];
    for (int i = 0; i < w.length; i++) {
        for (int j = C; j >= w[i]; j--) { // 必须倒序遍历
            dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
        }
    }
    return dp[C];
}
复制代码

七、动态规划解题方法论总结

1. 五步法流程

  1. 定义状态:明确dp数组的含义

  2. 推导转移方程:分析状态间的关系

  3. 初始化:设置边界条件

  4. 确定遍历顺序:保证前置状态已计算

  5. 输出结果:从dp数组中提取答案

2. 优化路线图

复制代码

3. 常见问题处理技巧

  • 边界条件处理:增加虚拟头节点(如dp[0])

  • 路径记录:使用额外数组保存选择路径

  • 维度压缩:滚动数组、位运算优化


八、实战练习建议

  1. 基础练习

  2. 进阶挑战


掌握动态规划的关键在于将递归思维转化为状态转移思维。建议从简单问题入手,逐步体会"重叠子问题"的特征,最终达到看到问题就能自然拆分状态的境界。

相关推荐
2401_827364561 小时前
迷宫【BFS+结构体\pair】
算法·宽度优先
GGGGGGGGGGGGGG.3 小时前
使用dockerfile创建镜像
java·开发语言
Bruce Jue3 小时前
算法刷题--贪心算法
算法·贪心算法
兮动人4 小时前
SpringBoot加载配置文件的优先级
java·spring boot·后端·springboot加载配置
我爱Jack4 小时前
HttpServletRequest 和 HttpServletResponse 区别和作用
java·spring·mvc
yyueshen4 小时前
volatile 在 JVM 层面的实现机制
java·jvm
慕容魏4 小时前
入门到入土,Java学习 day16(算法1)
java·学习·算法
m0_748254664 小时前
Spring Boot 热部署
java·spring boot·后端
mango02194 小时前
SpringMVC
java