动态规划
动态规划
动态规划(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]表示text1前i个字符和text2前j个字符的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) |
六、关键点
- 状态定义:明确
dp数组的含义。 - 转移方程:基于子问题关系推导状态转移。
- 初始化:处理边界条件(如第一行/列)。
- 遍历顺序:确保计算
dp[i][j]时依赖的状态已计算。

浙公网安备 33010602011771号