动态规划解题方法
Table of Contents
一、动态规划框架
1. 一些概念
动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离呀等等。动态规划求解依靠的手段也是穷举,但相比于一般的搜索(回溯等)有区别,因为这类问题都存在重叠子问题,从而可以利用DP table 来优化穷举过程。而且,动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。
动态规划与递归
动态规划是自底向上,递归树是自顶向下
为什么动态规划一般都脱离了递归,而是由循环迭代完成计算。
2. 最优子结构
经典问题:无权最短路径(最优子结构)、无权最长路径(不满足最优子结构)
最优子结构的理解非常重要,也有难度。
如果一个问题的最优解包含其子问题的最优解,我们就称此问题具有最优子结构。 ——算法导论,15.3 动态规划原理
通俗来说就是:定义一个问题 Q,求目标解 A。如果我们找出的目标解 A 的同时, Q 的子问题的目标解同时也被找到,那么称问题 Q 具有最优子结构
参考知乎上的一个回答。
举个简单的例子。下面是一个地图,我们要找一条从左下角(起点)到右上角(终点)、只向右和向上走的路径。
如果要让路径经过的数字总和最大,那么最优路径是下面这条:
可以验证,对于最优路径上的任意一点,最优路径从起点到该点的部分,也正是从起点到该点的所有路径中数字总和最大的那一条。这就叫「满足最优子结构」。
现在换一个「最优」的标准,要求路径经过的数字总和的绝对值最小。那么最优路径是下面这条:
但是,对于最优路径上 -4 这个点,最优路径从起点到该点的部分,却不是从起点到该点的所有路径中,数字总和的绝对值最小的那一条,因为下面这条路径上数字总和的绝对值更小:
这就叫「不满足最优子结构」。
常见的最优化问题,问法一般都是「最大」「最小」,不太会出现「绝对值最小」这种奇葩的最优化标准。而问「最大」「最小」的问题,一般都是满足最优子结构的。
3. 框架与步骤
- 状态定义:dp[i] 的值代表了什么…
- 转移方程:考虑有多少选择的可能,对每种选择各个状态转移的过程是怎么样的…
- 初始状态base case
- 返回值
|
|
二、状态定义
定义好状态,题目直接变清晰
-
最长递增子序列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
public int lengthOfLIS(int[] nums) { if (nums == null) { return 0; } int len = nums.length; int res = 0; int[] dp = new int[len]; for (int i = 0; i < len; i++) { dp[i] = 1; for (int j = 0; j < i; j++) { if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1); } res = Math.max(dp[i], res); } return res; }
-
最大子序和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public int maxSubArray(int[] nums) { int n = nums.length; int[] dp = new int[n]; dp[0] = nums[0]; int res = dp[0]; for (int i = 1; i < n; i++) { if (dp[i-1] > 0) { dp[i] = dp[i-1] + nums[i]; } else { dp[i] = nums[i]; } res = Math.max(dp[i], res); } return res; }
-
俄罗斯套娃信封问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
class Solution { public int maxEnvelopes(int[][] envelopes) { int n = envelopes.length; Arrays.sort(envelopes, new Comparator<int[]>() { public int compare(int[] a, int[] b) { return a[0] == b[0] ? b[1] - a[1] : a[0] - b[0]; } }); int[] height = new int[n]; for (int i = 0; i < n; i++) { height[i] = envelopes[i][1]; } return lengthOfLIS(height); } /* 返回 nums 中 LIS 的长度 */ public int lengthOfLIS(int[] nums) { int piles = 0, n = nums.length; int[] top = new int[n]; for (int i = 0; i < n; i++) { // 要处理的扑克牌 int poker = nums[i]; int left = 0, right = piles; // 二分查找插入位置 while (left < right) { int mid = (left + right) / 2; if (top[mid] >= poker) right = mid; else left = mid + 1; } if (left == piles) piles++; // 把这张牌放到牌堆顶 top[left] = poker; } // 牌堆数就是 LIS 长度 return piles; } }
三、状态转移
-
最长公共子序列
1 2 3 4 5 6 7 8 9 10 11 12 13 14
public int longestCommonSubsequence(String text1, String text2) { int n1 = text1.length(), n2 = text2.length(); int[][] dp = new int[n1 + 1][n2 + 1]; for (int i = 1; i <= n1; i++) { for (int j = 1; j <= n2; 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[n1][n2]; }
-
两个字符串的删除操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
class Solution { public int minDistance(String word1, String word2) { int m = word1.length(), n = word2.length(); int[][] dp = new int[m+1][n+1]; for (int i = 0; i <= n; i++) { dp[0][i] = i; } for (int i = 0; i <= m; i++) { dp[i][0] = i; } for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (word1.charAt(i-1) == word2.charAt(j-1)) { dp[i][j] = dp[i-1][j-1]; } else { dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + 1; } } } return dp[m][n]; } }
-
编辑距离
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
public int minDistance(String word1, String word2) { int m = word1.length(), n = word2.length(); int[][] dp = new int[m+1][n+1]; for (int i = 0; i <= n; i++) { dp[0][i] = i; } for (int i = 0; i <= m; i++) { dp[i][0] = i; } for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (word1.charAt(i-1) == word2.charAt(j-1)) { dp[i][j] = dp[i-1][j-1]; } else { dp[i][j] = Math.min(Math.min(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1; } } } return dp[m][n]; }