动态规划

动态规划

动态规划(Dynamic Programming,DP)通过将复杂问题分解为子问题,并存储子问题的解来避免重复计算,从而提高效率。


一、线性DP(一维动态规划)

核心思想:用一维数组 dp[] 表示状态,状态转移仅依赖前一维度的结果。

示例:斐波那契数列
  • 问题:求第 n 个斐波那契数(F(n) = F(n-1) + F(n-2))。

  • 状态定义dp[i] 表示第 i 个斐波那契数。

  • 状态转移dp[i] = dp[i-1] + dp[i-2]

  • 代码实现

    public int fibonacci(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),仅保留前两个值。


二、二维DP(二维动态规划)

核心思想:用二维数组 dp[][] 表示状态,状态转移依赖行和列两个维度。

示例:最小路径和
  • 问题:从网格左上角到右下角,找一条路径使得路径上的数字总和最小。

  • 状态定义dp[i][j] 表示从起点到 (i,j) 的最小路径和。

  • 状态转移

    dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
    
  • 代码实现

    public int minPathSum(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        int[][] dp = new int[m][n];
        dp[0][0] = grid[0][0];
        // 初始化第一行和第一列
        for (int i = 1; i < m; i++) dp[i][0] = dp[i-1][0] + grid[i][0];
        for (int j = 1; j < n; j++) dp[0][j] = dp[0][j-1] + grid[0][j];
        // 填充其他位置
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = grid[i][j] + Math.min(dp[i-1][j], dp[i][j-1]);
            }
        }
        return dp[m-1][n-1];
    }
    

三、最长递增子序列(LIS)

问题:找到数组中最长的递增子序列(不要求连续)。

动态规划解法
  • 状态定义dp[i] 表示以 nums[i] 结尾的最长递增子序列长度。

  • 状态转移

    dp[i] = max(dp[j]) + 1,其中 0 ≤ j < i 且 nums[j] < nums[i]
    
  • 代码实现

    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];
        Arrays.fill(dp, 1); // 初始化为1,每个元素自身是一个子序列
        int maxLen = 1;
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            maxLen = Math.max(maxLen, dp[i]);
        }
        return maxLen;
    }
    
  • 时间复杂度:O(n²)。

  • 优化:贪心 + 二分查找可将时间优化至 O(n log n)。


四、最长公共子序列(LCS)

问题:找到两个字符串的最长公共子序列(不要求连续)。

动态规划解法
  • 状态定义dp[i][j] 表示 text1i 个字符和 text2j 个字符的LCS长度。

  • 状态转移

    if (text1[i-1] == text2[j-1]) {
        dp[i][j] = dp[i-1][j-1] + 1;
    } else {
        dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
    }
    
  • 代码实现

    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length(), n = text2.length();
        int[][] dp = new int[m+1][n+1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (text1.charAt(i-1) == text2.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }
    
  • 时间复杂度:O(mn),空间复杂度可优化至 O(n)。


五、总结对比

问题类型 状态定义 状态转移 时间复杂度
线性DP 一维数组 dp[i] 依赖前一两个状态(如斐波那契) O(n)
二维DP 二维数组 dp[i][j] 依赖行和列的前驱状态(如路径和) O(mn)
LIS 一维数组 dp[i] 遍历前面所有更小元素 O(n²)
LCS 二维数组 dp[i][j] 根据字符是否相等转移 O(mn)

六、关键点

  1. 状态定义:明确 dp 数组的含义。
  2. 转移方程:基于子问题关系推导状态转移。
  3. 初始化:处理边界条件(如第一行/列)。
  4. 遍历顺序:确保计算 dp[i][j] 时依赖的状态已计算。
posted @ 2025-02-24 20:18  咋还没来  阅读(66)  评论(0)    收藏  举报