动态规划
[TOC]
单串问题
5.最长回文子串
-
解题要点:二维动态规划,通过
dp[j + 1][i - 1]
推导dp[j][i]
public String longestPalindrome(String s) { if (s == null || s.length() == 0) { return ""; } String result = String.valueOf(s.charAt(0)); boolean[][] dp = new boolean[s.length()][s.length()]; for (int i = 0; i < s.length(); i++) { dp[i][i] = true; } for (int i = 1; i < s.length(); i++) { for (int j = i - 1; j >= 0; j--) { if ((dp[j + 1][i - 1] || i - j < 2) && s.charAt(j) == s.charAt(i)) { dp[j][i] = true; if (i - j + 1 > result.length()) { result = s.substring(j, i + 1); } } } } return result; }
300.最长递增子序列
-
二维动态规划经典问题。dp[i] = max{dp[j], 0 <= j < i}
public int lengthOfLIS(int[] nums) { int[] dp = new int[nums.length]; Arrays.fill(dp, 1); int maxLen = dp[0]; for (int i = 1; i < nums.length; i++) { for (int j = 0; j < i; j++) { if (nums[i] > nums[j]) { dp[i] = Math.max(dp[i], dp[j] + 1); maxLen = Math.max(maxLen, dp[i]); } } } return maxLen; }
-
上述解法通过构造不同含义的动态规划方程并借助二分查找优化时间复杂度为
O(nlogn)
public int lengthOfLIS(int[] nums) { int[] dp = new int[nums.length]; //dp[i] 表示 i 个元素的子序列最后一个元素的最小值 int res = 0; for (int num: nums) { int l = 0, r = res; while (l < r) { int m = l + (r - l) / 2; if (dp[m] < num) { l = m + 1; } else { r = m; } } dp[l] = num; // dp[l] <= num <= dp[r] if (r == res) { // num > dp[r],子序列递增一个。 res++; } } return res; }
673. 最长递增子序列的个数
-
求最长递增子序列,同时计算每个长度递增子序列的长度。
public int findNumberOfLIS(int[] nums) { if (nums == null || nums.length == 0) { return 0; } int[] length = new int[nums.length]; int[] count = new int[nums.length]; length[0] = 1; count[0] = 1; for (int i = 1; i < nums.length; i++) { length[i] = 1; count[i] = 1; for (int j = i - 1; j >= 0; j--) { if (nums[i] > nums[j]) { if (length[i] < length[j] + 1) { // 以 num[i] 结尾的子序列长度有增长 length[i] = length[j] + 1; count[i] = count[j]; // 拼接 以 num[j] 结尾的最长子序列 } else if(length[i] == length[j] + 1) { count[i] += count[j]; // 新增 count[j] 个长度为 length[i] 的子序列 } } } } int maxLength = Arrays.stream(length).max().getAsInt(); int res = 0; for (int i = 0; i < nums.length; i++) { if (length[i] == maxLength) { res += count[i]; } } return res; }
354. 俄罗斯套娃信封问题
- 先排序,然后在数组第二个维度上子最长递增子序列问题。
public int maxEnvelopes(int[][] envelopes) {
Arrays.sort(envelopes, (a, b) -> {
if (a[0] == b[0]) {
return b[1] - a[1];
}
return a[0] - b[0];
});
int[] dp = new int[envelopes.length];
Arrays.fill(dp, 1);
int maxLen = dp[0];
for (int i = 1; i < envelopes.length; i++) {
for (int j = 0; j < i; j++) {
if (envelopes[i][1] > envelopes[j][1]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
maxLen = Math.max(maxLen, dp[i]);
}
}
}
return maxLen;
}
53. 最大子序和
-
dp[i] 表示以 num[i] 结尾的最大子数组和。
public int maxSubArray(int[] nums) { int[] dp = new int[nums.length]; dp[0] = nums[0]; int maxSum = dp[0]; for (int i = 1; i < nums.length; i++) { dp[i] = Math.max(dp[i - 1], 0) + nums[i]; maxSum = Math.max(maxSum, dp[i]); } return maxSum; }
152. 乘积最大子数组
-
与最大子序和类似,乘法需要考虑正负号,所以需要最大 dp 数组和最小 dp 数组。
public int maxProduct(int[] nums) { int[] minProduct = new int[nums.length]; int[] maxProduct = new int[nums.length]; minProduct[0] = nums[0]; maxProduct[0] = nums[0]; int maxRes = maxProduct[0]; for (int i = 1; i < nums.length; i++) { maxProduct[i] = Math.max(maxProduct[i - 1] * nums[i], Math.max(nums[i], minProduct[i - 1] * nums[i])); minProduct[i] = Math.min(minProduct[i - 1] * nums[i], Math.min(nums[i], maxProduct[i - 1] * nums[i])); maxRes = Math.max(maxRes, maxProduct[i]); } return maxRes; }
918. 环形子数组的最大和
-
分两种情况:
- 不同时包含首尾元素,直接求最大子数组和
- 同时包含首尾元素,求 num[1...length - 2] 最小子数组和,然后用整个数组和相减。
public int maxSubarraySumCircular(int[] A) { if (A.length < 2) { return A[0]; } int maxSum = A[0]; int curr = A[0]; int sum = A[0]; for (int i = 1; i < A.length; i++) { curr = Math.max(curr + A[i], A[i]); maxSum = Math.max(curr, maxSum); sum += A[i]; } int minSum = A[1]; curr = A[1]; for (int i = 2; i < A.length - 1; i++) { curr = Math.min(curr + A[i], A[i]); minSum = Math.min(minSum, curr); } return Math.max(sum - minSum, maxSum); }
面试题 17.24. 最大子矩阵
-
将第 i...j 行的矩阵的列方向和求出,然后在这个列向和数组上求最大子数组和。
public int[] getMaxMatrix(int[][] matrix) { int[] result = new int[4]; int maxSum = matrix[0][0]; for (int i = 0; i < matrix.length; i++) { int[] sum = new int[matrix[i].length]; for (int j = i; j < matrix.length; j++) { int start = 0; int curr = 0; for (int k = 0; k < matrix[i].length; k++) { sum[k] += matrix[j][k]; curr += sum[k]; if (curr > maxSum) { result[0] = i; result[1] = start; result[2] = j; result[3] = k; maxSum = curr; } if (curr < 0) { start = k + 1; curr = 0; } } } } return result; }
363. 矩形区域不超过 K 的最大数值和
-
子矩阵求和,然后通过 TreeSet 保存 sum[0...i] ,找到 i,j 使得 sum[j] - sum[i] <= k。
public int maxSumSubmatrix(int[][] matrix, int k) { int ans = Integer.MIN_VALUE; for (int i = 0; i < matrix.length; i++) { int[] sum = new int[matrix[i].length]; for (int j = i; j < matrix.length; j++) { int curr = 0; TreeSet
sumSet = new TreeSet<>(); sumSet.add(0); for (int col = 0; col < matrix[i].length; col++) { sum[col] += matrix[j][col]; curr += sum[col]; Integer tmp = sumSet.ceiling(curr - k); if (tmp != null) { ans = Math.max(ans, curr - tmp); } sumSet.add(curr); } } } return ans; }
873. 最长的斐波那契子序列的长度
与最长递增子序列类似,斐波那契子序列需要向前看两位。
-
朴素的动态规划时间复杂度 O(n^3),可通过 HashMap 降低至 O(n^2)。
public int lenLongestFibSubseq(int[] arr) { int len = arr.length; Map
indexMap = new HashMap<>(); for (int i = 0; i < len; i++) { indexMap.put(arr[i], i); } int[][] dp = new int[len][len]; int max = 0; for (int i = 2; i < len; i++) { for (int j = i - 1; j >= 0; j--) { int k = indexMap.getOrDefault(arr[i] - arr[j], -1); if (k >= 0 && k < j) { dp[j][i] = dp[k][j] + 1; max = Math.max(max, dp[j][i] + 2); } } } return max < 3 ? 0 : max; }
1027. 最长等差数列
-
解题思路同最长递增子序列,dp[i][j] 表示以 nums[i] 和 nums[j] 为首尾的等差数列。
public int longestArithSeqLength(int[] nums) { int[][] dp = new int[nums.length][nums.length]; Map
indexMap = new HashMap<>(); int ans = 2; for (int i = 0; i < nums.length; i++) { for (int j = i + 1; j < nums.length; j++) { Integer k = indexMap.getOrDefault(2 * nums[i] - nums[j], -1); if (k >= 0 && k < i) { dp[i][j] = Math.max(dp[i][j], dp[k][i] + 1); ans = Math.max(ans, dp[i][j] + 2); } } indexMap.put(nums[i], i); } return ans; }
368. 最大整除子集
排序数组使得最大整除子集的最大值下标最大。
-
通过一维数组存储整除子集的下标。
public List
largestDivisibleSubset(int[] nums) { Arrays.sort(nums); int[] dp = new int[nums.length]; Arrays.fill(dp, 1); int[] index = new int[nums.length]; for (int i = 0; i < index.length; i++) { index[i] = i; } int maxIndex = 0; for (int i = 1; i < nums.length; i++) { for (int j = i - 1; j >= 0; j--) { if (nums[i] % nums[j] == 0 && dp[i] < dp[j] + 1) { dp[i] = dp[j] + 1; index[i] = j; if (dp[maxIndex] < dp[i]) { maxIndex = i; } } } } List result = new ArrayList<>(); for (int i = 0, idx = maxIndex; i < dp[maxIndex]; i++) { result.add(0, nums[idx]); idx = index[idx]; } return result; }
32. 最长有效括号
-
dp[i] 表示 0...i 最长有效括号,dp[i] 根据 s.charAt(i) == ')' 分情况讨论。
public int longestValidParentheses(String s) { int[] dp = new int[s.length()]; int maxLen = 0; for (int i = 1; i < s.length(); i++) { if (s.charAt(i) == ')') { if (s.charAt(i - 1) == '(') { dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2; } else { if (i - dp[i - 1] - 1 >= 0 && s.charAt(i - dp[i - 1] - 1) == '(') { dp[i] = dp[i - 1] + 2 + (i - dp[i - 1] - 2 >= 0 ? dp[i - dp[i - 1] - 2] : 0); } } } maxLen = Math.max(maxLen, dp[i]); } return maxLen; }
-
此题还可以通过栈解决。
public int longestValidParentheses(String s) { int maxans = 0; Deque
stack = new LinkedList (); stack.push(-1); for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == '(') { stack.push(i); } else { stack.pop(); if (stack.isEmpty()) { stack.push(i); } else { maxans = Math.max(maxans, i - stack.peek()); } } } return maxans; }
413. 等差数列划分
dp[i] 表示以 num[i] 结尾的子数组的等差数列个数。
-
dp 数组的和就是所有等差子数组的个数。
public int numberOfArithmeticSlices(int[] nums) { int[] dp = new int[nums.length]; int res = 0; for (int i = 2; i < nums.length; i++) { if (2 * nums[i - 1] == nums[i - 2] + nums[i]) { dp[i] = dp[i - 1] + 1; } res += dp[i]; } return res; }
91. 解码方法
-
递归解法会超时,使用动态规划,dp[i] 表示 s(0...i) 的解码方法。
public int numDecodings(String s) { if (s == null || s.length() == 0) { return 0; } int[] dp = new int[s.length()]; dp[0] = s.charAt(0) == '0' ? 0 : 1; for (int i = 1; i < s.length(); i++) { if (s.charAt(i) == '0') { if (s.charAt(i - 1) == '1' || s.charAt(i - 1) == '2') { dp[i] = i == 1 ? 1 : dp[i - 2]; } else { return 0; } } else { String subStr = s.substring(i - 1, i + 1); if (subStr.compareTo("10") > 0 && subStr.compareTo("26") <= 0) { dp[i] = dp[i-1] + (i == 1 ? 1 : dp[i - 2]); } else { dp[i] = dp[i - 1]; } } } return dp[s.length() - 1]; }
132. 分割回文串 II
-
先求出所有的回文串dp[i][j],基于 dp 数组再求 min[0...i] 最少分割次数。
public int minCut(String s) { boolean[][] dp = new boolean[s.length()][s.length()]; for (int i = 0; i < s.length(); i++) { dp[i][i] = true; } for (int i = 1; i < s.length(); i++) { for (int j = 0; j < i; j++) { if (s.charAt(i) == s.charAt(j) &&(i - j < 2 || dp[j + 1][i - 1])) { dp[j][i] = true; } } } int[] minNum = new int[s.length()]; for (int i = 0; i < s.length(); i++) { minNum[i] = i; } for (int i = 1; i < s.length(); i++) { for (int j = i - 1; j >= 0; j--) { if (dp[j][i]) { minNum[i] = Math.min(minNum[i], j == 0 ? 0 : minNum[j - 1] + 1); } else { minNum[i] = Math.min(minNum[i], minNum[i - 1] + 1); } } } return minNum[s.length() - 1]; }
单串问题之打家劫舍系列
198.打家劫舍
-
打家劫舍最简单情形,相邻单位不能同时偷盗,动态规划方程
dp[i] = max(dp[i - 1], dp[i -2] + nums[i])
。public int rob(int[] nums) { if (nums.length < 2) { return nums[0]; } int[] dp = new int[nums.length]; dp[0] = nums[0]; dp[1] = Math.max(nums[0], nums[1]); for (int i = 2; i < nums.length; i++) { dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]); } return dp[nums.length - 1]; }
213.打家劫舍 II
-
所有房子连成环,首尾不能同时偷盗,所以分开考虑。
public int rob(int[] nums) { if (nums.length < 2) { return nums[0]; } return Math.max(rob(nums, 0, nums.length - 2), rob(nums, 1, nums.length - 1)); } public int rob(int[] nums, int start, int end) { if (start == end) { return nums[start]; } int[] dp = new int[nums.length]; dp[start] = nums[start]; dp[start + 1] = Math.max(nums[start], nums[start + 1]); for (int i = start + 2; i <= end; i++) { dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]); } return dp[end]; }
740. 删除并获得点数
选择一个数字 num 之后,对应的相同数字全部被选择。
-
num - 1 和 num + 1 不能被选择,即打家劫舍的变形,dp[i] 表示数字 0...i 中能选择的最大点数。
public int deleteAndEarn(int[] nums) { int maxNum = Arrays.stream(nums).max().getAsInt(); int[] count = new int[maxNum + 1]; for (int num : nums) { count[num]++; } int[] dp = new int[count.length]; dp[0] = 0; dp[1] = count[1]; for (int i = 2; i < count.length; i++) { dp[i] = Math.max(dp[i - 1], dp[i - 2] + count[i] * i); } return dp[count.length - 1]; }
1388.3n 块披萨
动态规划,与环装打家劫舍比较类似。
-
动态规划方程
dp[i][j]
表示在 0 ... i 块披萨中选 j 块披萨获得的价值。public int maxSizeSlices(int[] slices) { if (slices.length <= 3) { return Arrays.stream(slices).max().getAsInt(); } return Math.max(maxSizeSlices(slices, 0, slices.length -2), maxSizeSlices(slices, 1, slices.length - 1)); } public int maxSizeSlices(int[] nums, int start, int end) { int n = nums.length / 3; int[][] dp = new int[nums.length][n + 1]; dp[start][1] = nums[start]; dp[start + 1][1] = Math.max(nums[start], nums[start + 1]); for (int i = start + 2; i <= end; i++) { for (int j = 1; j <= n; j++) { dp[i][j] = Math.max(dp[i - 1][j], dp[i - 2][j - 1] + nums[i]); } } return dp[end][n]; }
单串问题之股票买卖系列
121. 买卖股票的最佳时机
-
只能买卖一次, dp[i][0] 表示第 i 天未持有股票的收益,dp[i][1] 表示第 i 天持有骨片的收益。
public int maxProfit(int[] prices) { int[][] profits = new int[prices.length][2]; profits[0][1] = - prices[0]; for (int i = 1; i < prices.length; i++) { profits[i][0] = Math.max(profits[i - 1][0], profits[i - 1][1] + prices[i]); profits[i][1] = Math.max(profits[i - 1][1], - prices[i]); } return profits[prices.length - 1][0]; }
122. 买卖股票的最佳时机 II
-
可以进行无数次买卖,所以计算第 i 天持有股票的收益时,需要考虑第 i - 1 天未持有股票的情况。
public int maxProfit(int[] prices) { int[][] profits = new int[prices.length][2]; profits[0][1] = - prices[0]; for (int i = 1; i < prices.length; i++) { profits[i][0] = Math.max(profits[i - 1][0], profits[i - 1][1] + prices[i]); profits[i][1] = Math.max(profits[i - 1][1], profits[i - 1][0] - prices[i]); } return profits[prices.length - 1][0]; }
123. 买卖股票的最佳时机 III
最多进行两次交易,需要将 dp 数组扩充到三维,记录进行了多少次交易。
-
注意 dp 数组的初始化。
public int maxProfit(int[] prices) { int[][][] profits = new int[prices.length][2][3]; profits[0][1][1] = - prices[0]; profits[0][1][2] = Integer.MIN_VALUE; for (int i = 1; i < prices.length; i++) { for (int j = 2; j >= 1; j--) { profits[i][0][j] = Math.max(profits[i - 1][0][j], profits[i - 1][1][j] + prices[i]); profits[i][1][j] = Math.max(profits[i - 1][1][j], profits[i - 1][0][j - 1] - prices[i]); } } return Arrays.stream(profits[prices.length - 1][0]).max().getAsInt(); }
188. 买卖股票的最佳时机 IV
最多进行 k 次交易,最通用的情况。
当 k > n / 2 时,变为无限次交易的情况,特殊处理。
public int maxProfit(int k, int[] prices) {
if (k == 0) {
return 0;
}
if (k > prices.length / 2) {
return maxProfit(prices);
}
int[][][] profits = new int[prices.length][2][k + 1];
profits[0][1][1] = - prices[0];
for (int i = 2; i <= k; i++) {
profits[0][1][i] = Integer.MIN_VALUE;
}
for (int i = 1; i < prices.length; i++) {
for (int j = 1; j <= k; j++) {
profits[i][0][j] = Math.max(profits[i - 1][0][j], profits[i - 1][1][j] + prices[i]);
profits[i][1][j] = Math.max(profits[i - 1][1][j], profits[i - 1][0][j - 1] - prices[i]);
}
}
return Arrays.stream(profits[prices.length - 1][0]).max().getAsInt();
}
public int maxProfit(int[] prices) {
if (prices.length == 0) {
return 0;
}
int[][] profits = new int[prices.length][2];
profits[0][1] = - prices[0];
for (int i = 1; i < prices.length; i++) {
profits[i][0] = Math.max(profits[i - 1][0], profits[i - 1][1] + prices[i]);
profits[i][1] = Math.max(profits[i - 1][1], profits[i - 1][0] - prices[i]);
}
return profits[prices.length - 1][0];
}
309. 最佳买卖股票时机含冷冻期
-
冷冻期即表示计算 dp[i] 时,需要向前看一天。
public int maxProfit(int[] prices) { if (prices.length == 0) { return 0; } int[][] profits = new int[prices.length][2]; profits[0][1] = - prices[0]; for (int i = 1; i < prices.length; i++) { profits[i][0] = Math.max(profits[i - 1][0], profits[i - 1][1] + prices[i]); profits[i][1] = Math.max(profits[i - 1][1], (i == 1 ? 0 : profits[i - 2][0]) - prices[i]); } return profits[prices.length - 1][0]; }
714. 买卖股票的最佳时机含手续费
-
卖掉股票的时候减掉手术费。
public int maxProfit(int[] prices, int fee) { if (prices.length == 0) { return 0; } int[][] profits = new int[prices.length][2]; profits[0][1] = - prices[0]; for (int i = 1; i < prices.length; i++) { profits[i][0] = Math.max(profits[i - 1][0], profits[i - 1][1] + prices[i] - fee); profits[i][1] = Math.max(profits[i - 1][1], profits[i - 1][0] - prices[i]); } return profits[prices.length - 1][0]; }
双串问题
1143.最长公共子序列
-
关键在于构造动态规划方程。
dp[i][j] = 最长公共子序列 { text1[0 : i], text2[0 : j]}
public int longestCommonSubsequence(String text1, String text2) { int len1 = text1.length(); int len2 = text2.length(); int[][] dp = new int[len1 + 1][len2 + 1]; for (int i = 1; i <= len1; i++) { for (int j = 1; j <= len2; 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[len1][len2]; }
1713.得到子序列的最少操作次数
-
由于
target
中元素互不相同,故将arr
数组映射到target
数组元素坐标上,将问题转化为求两个映射下标数组的最长公共子序列, 而target
映射下标数组是严格递增的,进一步将问题转化为求arr
映射的下标数组最长递增子序列问题。public int minOperations(int[] target, int[] arr) { HashMap
index = new HashMap<>(); for (int i = 0; i < target.length; i++) { index.put(target[i], i); } int[] dp = new int[arr.length]; int maxLen = 0; for (int num: arr) { Integer numIndex = index.get(num); if (numIndex != null) { int l = 0, r = maxLen; while (l < r) { int m = l + (r - l) / 2; if (dp[m] < numIndex) { l = m + 1; } else { r = m; } } dp[l] = numIndex; if (r == maxLen) { maxLen++; } } } return target.length - maxLen; }
动态规划方程难度 ⭐️⭐️⭐️⭐️⭐️
887.鸡蛋掉落
方程比较难想出来
朴素的动态规划会超时,需要借助二分查找优化时间复杂度。
-
二分查找的思想来自于单调直线函数“谷底”极值。
public int superEggDrop(int k, int n) { int[][] dp = new int[n+ 1][k + 1]; for (int i = 1; i <= n; i++) { Arrays.fill(dp[i], i); } for (int i = 2; i <= n; i++) { for (int j = 2; j <= k; j++) { int lo = 1, hi = i; while (lo < hi) { int mid = lo + (hi - lo) / 2; if (dp[mid - 1][j - 1] < dp[i - mid][j]) { lo = mid + 1; } else { hi = mid; } } dp[i][j] = Math.max(dp[lo - 1][j - 1], dp[i - lo][j]) + 1; } } return dp[n][k]; }