Arithmetic Slices
算术片的个数
思路(最优解):序列型dp。每次记录一下以当前数为末尾的算术片的最大长度以及数的等差值。下一次就能求出算术片增加的个数。使用滚动指针来优化。时间复杂度O(n),空间复杂度O(1)。
1 public class Solution { 2 public int numberOfArithmeticSlices(int[] A) { 3 if (A.length < 3) { 4 return 0; 5 } 6 7 int prev_res = 0; 8 int prev_length = 2; 9 int prev_diff = A[1] - A[0]; 10 11 for (int i = 2; i < A.length; i++) { 12 if (prev_diff == A[i] - A[i - 1]) { 13 prev_res += prev_length - 1; 14 prev_length++; 15 } else { 16 prev_length = 2; 17 prev_diff = A[i] - A[i - 1]; 18 } 19 } 20 return prev_res; 21 } 22 }
Climbing Stairs
爬楼梯
思路:滚动指针优化到O(1)空间。开一个大小为2的数组,用模2方式来求解。
1 public class Solution { 2 public int climbStairs(int n) { 3 if (n == 1) { 4 return 1; 5 } 6 int[] dp = new int[2]; 7 dp[0] = 1; 8 dp[1] = 2; 9 10 for (int i = 2; i < n; i++) { 11 dp[i % 2] = dp[(i - 1) % 2] + dp[(i - 2) % 2]; 12 } 13 return dp[(n - 1) % 2]; 14 } 15 }
Drop Eggs II
扔鸡蛋II
思路:状态:设一种丢法在最坏情况下需要丢p次能确定结果,状态dp[i][j]表示i个鸡蛋和j层楼的所有丢法的最小p值。
方程:对于i个蛋,j层楼,要确定它的所有丢法的最小p值。先假定第一次在第k层丢(1<=k<=j),如果蛋碎了,那么我们只需要用剩下的鸡蛋测试k层以下的楼层,所以问题简化为k-1层和i-1个鸡蛋,因此对应的次数是dp[i-1][k-1]+1;如果蛋不碎,那么我们只需要检查比k较高的楼层; 所以问题简化为 j-k 和i个鸡蛋,即dp[i][j-k]+1。最坏情况应该是两者的最大值。而所有k(1~j)对应的次数的最小值就是最小p值。
初始化:对于一个蛋的问题,p值是楼层数j。即dp[1][j] = j.
结果:dp[m][n]
public class Solution { /** * @param m the number of eggs * @param n the umber of floors * @return the number of drops in the worst case */ public int dropEggs2(int m, int n) { // Write your code here int[][] dp = new int[m+1][n+1]; for (int j = 1; j <= n; j++) { dp[1][j] = j; } for (int i = 2; i <= m; i++) { for (int j = 1; j <= n; j++) { dp[i][j] = Integer.MAX_VALUE; for (int first = 1; first <= j; first++) { dp[i][j] = Math.min(Math.max(dp[i-1][first - 1] + 1, dp[i][j - first] + 1), dp[i][j]); } } } return dp[m][n]; } }
Is Subsequence
判断是否是子序列
思路1:典型的双序列动态规划。 状态dp[i][j]表示s的前i个字符是否是t的前j个字符的子序列。时间复杂度O(s*t),空间复杂度O(s*t),使用滚动数组可以讲空间优化到O(t)。
1 public class Solution { 2 public boolean isSubsequence(String s, String t) { 3 boolean[][] dp = new boolean[s.length() + 1][t.length() + 1]; 4 5 for (int j = 0; j <= t.length(); j++) { 6 dp[0][j] = true; 7 } 8 9 for (int i = 1; i <= s.length(); i++) { 10 for (int j = 1; j <= t.length(); j++) { 11 if (dp[i][j - 1]) { 12 dp[i][j] = true; 13 } else { 14 dp[i][j] = dp[i - 1][j - 1] && s.charAt(i - 1) == t.charAt(j - 1); 15 } 16 } 17 } 18 return dp[s.length()][t.length()]; 19 } 20 }
思路2:使用two pointers。时间复杂度O(t),空间复杂度O(1)。
1 public class Solution { 2 public boolean isSubsequence(String s, String t) { 3 if (s.length() == 0) return true; 4 int indexS = 0, indexT = 0; 5 while (indexT < t.length()) { 6 if (t.charAt(indexT) == s.charAt(indexS)) { 7 indexS++; 8 if (indexS == s.length()) return true; 9 } 10 indexT++; 11 } 12 return false; 13 } 14 }
思路3(最优解):改进的two pointers,使用indexOf函数,时间复杂度仍然是O(t)。但是思路2调用了t次charAt函数,而这个方法只调用了s次函数。虽然时间复杂度相同,但是实际运行起来更快。
1 public class Solution { 2 public boolean isSubsequence(String s, String t) { 3 if (s.length() == 0) return true; 4 int pt = 0; 5 for (int ps = 0; ps < s.length(); ps++) { 6 char c = s.charAt(ps); 7 pt = t.indexOf(c, pt); 8 if (pt == -1) { 9 return false; 10 } 11 pt++; 12 } 13 return true; 14 } 15 }
k Sum
k数和
思路:一道三维动态规划,状态和方程都比较容易想,初始化要想一下。
public class Solution { /** * @param A: an integer array. * @param k: a positive integer (k <= length(A)) * @param target: a integer * @return an integer */ public int kSum(int A[], int k, int target) { // write your code here int[][][] dp = new int[A.length + 1][k + 1][target + 1]; for (int i = 1; i < A.length; i++) { for (int m = 1; m < target; m++) { if (A[i - 1] == m) { dp[i][1][m] = 1; } else { dp[i][1][m] = dp[i - 1][1][m]; } } } for (int i = 2; i <= A.length; i++) { for (int j = 2; j <= Math.min(i, k); j++) { for (int m = 1; m <= target; m++) { if (A[i - 1] < m) { dp[i][j][m] = dp[i - 1][j - 1][m - A[i - 1]]; } dp[i][j][m] += dp[i - 1][j][m]; } } } return dp[A.length][k][target]; } }
Longest Increasing Subsequence
最长递增子序列
思路1:LIS是一道经典的dp题。用O(n^2)来做是一道简单的序列型动态规划。
1 public class Solution { 2 public int lengthOfLIS(int[] nums) { 3 if (nums == null || nums.length == 0) { 4 return 0; 5 } 6 7 int[] dp = new int[nums.length]; 8 dp[0] = 1; 9 for (int i = 1; i < dp.length; i++) { 10 dp[i] = 1; 11 for (int j = 0; j <= i - 1; j++) { 12 if (nums[j] < nums[i]) { 13 dp[i] = Math.max(dp[i], 1 + dp[j]); 14 } 15 } 16 } 17 int res = 0; 18 for (int i = 0; i < dp.length; i++) { 19 res = Math.max(res, dp[i]); 20 } 21 return res; 22 } 23 }
思路2(最优解):这道题还存在O(nlgn)的解法。在思路1中,每次我们计算dp[i]的时候,都要往前面遍历找到一个nums[j],使得nums[j] < nums[i]并且dp[j]值最大。在这个过程中其实有些数我们不用找。比如说长度为2的子序列有(3,5),(3,10),(2,4),那么我们只需要与4比较就可以,而不需要与5和10比较。因此我们定义一个数组B,其中B[i]表示长度为i的所有子序列中最小的末尾数。可以证明B是一个单调递增的数组。因此每次我们只需要使用二分查找找到小于nums[i]的最大值,返回它的长度i即可。
1 public class Solution { 2 public int lengthOfLIS(int[] nums) { 3 if (nums == null || nums.length == 0) { 4 return 0; 5 } 6 7 int res = 0; 8 int[] B = new int[nums.length + 1]; 9 for (int i = 0; i < B.length; i++) { 10 B[i] = Integer.MAX_VALUE; 11 } 12 13 for (int i = 0; i < nums.length; i++) { 14 int len = binarySearch(B, nums[i]) + 1; 15 res = Math.max(res, len); 16 B[len] = Math.min(B[len], nums[i]); 17 } 18 19 return res; 20 } 21 22 public int binarySearch(int[] B, int k) { 23 int start = 0; 24 int end = B.length - 1; 25 while (start + 1 < end) { 26 int mid = (start + end) / 2; 27 if (B[mid] >= k) { 28 end = mid; 29 } else { 30 start = mid; 31 } 32 } 33 if (B[end] < k) { 34 return end; 35 } 36 if (B[start] < k) { 37 return start; 38 } 39 return 0; 40 } 41 }
Minimum Adjustment Cost
最小调整代价
思路:状态:dp[i][j]表示对0~i的数做调整,最后一个数调整到数j的最小代价。
方程:dp[i][j] = min(dp[i-1][m]) + cost(i,j),即序列0~i-1做调整,最后一个数调整到m的代价的最小值,加上第i个数调整到j的代价。为保证满足条件,m的范围是(j - taget ~ j + target)。
初始化:对于dp[0][j],就是与j的差值。
结果:dp矩阵最后一行的最小值。
public class Solution { /** * @param A: An integer array. * @param target: An integer. */ public int MinAdjustmentCost(ArrayList<Integer> A, int target) { // write your code here int[][] dp = new int[A.size()][101]; for (int j = 1; j < 101; j++) { dp[0][j] = Math.abs(j - A.get(0)); } for (int i = 1; i < A.size(); i++) { for (int j = 1; j < 101; j++) { int cost = Math.abs(j - A.get(i)); int min = Math.max(1, j - target); int max = Math.min(100, j + target); int pre_min_cost = Integer.MAX_VALUE; for (int m = min; m <= max; m++) { pre_min_cost = Math.min(dp[i-1][m], pre_min_cost); } dp[i][j] = pre_min_cost + cost; } } int res = Integer.MAX_VALUE; for (int j = 1; j < 101; j++) { res = Math.min(dp[A.size() - 1][j], res); } return res; } }
Partition Equal Subset Sum
和相同的二划分
思路1:每个划分的和应该是总和的一半,问题转化为01背包问题。状态dp[i][j]表示前j个数能否恰好装满容量为j的背包。
1 public class Solution { 2 public boolean canPartition(int[] nums) { 3 int sum = 0; 4 for (int num : nums) { 5 sum += num; 6 } 7 if (sum % 2 != 0) { 8 return false; 9 } 10 11 //转化为01背包问题 12 int capacity = sum / 2; 13 boolean[][] dp = new boolean[capacity + 1][nums.length + 1]; 14 15 dp[0][0] = true; 16 for (int i = 0; i <= capacity; i++) { 17 for (int j = 1; j <= nums.length; j++) { 18 if (nums[j - 1] <= i) { 19 dp[i][j] = dp[i - nums[j - 1]][j - 1] || dp[i][j - 1]; 20 } else { 21 dp[i][j] = dp[i][j - 1]; 22 } 23 } 24 } 25 26 return dp[capacity][nums.length]; 27 28 } 29 }
思路2(最优解):将思路1用滚动数组优化,这里在循环时要将背包容量i改为从大到小循环(见01背包问题的空间优化)。
1 public class Solution { 2 public boolean canPartition(int[] nums) { 3 int sum = 0; 4 for (int num : nums) { 5 sum += num; 6 } 7 if (sum % 2 != 0) { 8 return false; 9 } 10 11 int capacity = sum / 2; 12 boolean[] dp = new boolean[capacity + 1]; 13 14 dp[0] = true; 15 for (int j = 1; j <= nums.length; j++) { 16 for (int i = capacity; i >= 0; i--) { 17 if (nums[j - 1] <= i) { 18 dp[i] = dp[i - nums[j - 1]] || dp[i]; 19 } 20 } 21 } 22 23 return dp[capacity]; 24 25 } 26 }
Regular Expression Matching
正则表达式匹配
思路:是一道双序列型动态规划,注意方程与通配符匹配的不同之处。
1 public class Solution { 2 public boolean isMatch(String s, String p) { 3 int m = s.length(), n = p.length(); 4 char[] ws = s.toCharArray(); 5 char[] wp = p.toCharArray(); 6 boolean[][] dp = new boolean[m+1][n+1]; 7 8 dp[0][0] = true; 9 if (m > 0 && n > 0) { 10 dp[1][1] = wp[0] == '.' || wp[0] == ws[0]; 11 } 12 13 for (int j = 1; j <= n; j++) { 14 if (wp[j - 1] == '*') { 15 dp[0][j] = j == 1 ? true : dp[0][j - 2] || dp[0][j - 1]; 16 } 17 } 18 for (int i = 1; i <= m; i++) { 19 dp[i][0] = false; 20 } 21 22 for (int i = 1; i <= m; i++) { 23 for (int j = 2; j <= n; j++) { 24 if (wp[j-1] == '.' || ws[i-1] == wp[j-1]) { 25 dp[i][j] = dp[i-1][j-1]; 26 } else if (wp[j-1] == '*') { 27 if (wp[j-2] == '*') { 28 dp[i][j] = dp[i][j-1]; 29 } else { 30 dp[i][j] = dp[i][j-2]; 31 if (wp[j-2] == ws[i-1] || wp[j-2] == '.') { 32 dp[i][j] |= dp[i-1][j]; 33 } 34 } 35 } 36 } 37 } 38 return dp[m][n]; 39 } 40 }
Word Break II
切分单词II
思路:用wordbreak I 动态规划的结果,来为搜索做剪枝,避免不必要的搜索,保留isWord[i][j]数组可以避免每次重复的切割字符串。如果用动态规划中保留中间结果的方法会超时,因为保留了很多不必要的中间结果,而保留中间结果既耗时又耗空间。
public class Solution { /** * @param s a string * @param wordDict a set of words */ public List<String> wordBreak(String s, Set<String> wordDict) { // Write your code here boolean[] dp = new boolean[s.length() + 1]; int maxLen = 0; for (String word : wordDict) { maxLen = Math.max(word.length(), maxLen); } boolean[][] isWord = new boolean[s.length()][s.length()]; for (int i = 0; i < s.length(); i++) { for (int j = i; j < s.length(); j++) { String word = s.substring(i, j + 1); isWord[i][j] = wordDict.contains(word); } } dp[0] = true; for (int i = 1; i <= s.length(); i++) { for (int j = Math.max(0, i - maxLen); j < i; j++) { if (dp[j]) { if (isWord[j][i - 1]) { dp[i] = true; break; } } } } List<String> res = new ArrayList<String>(); search(res, isWord, dp, s.length() - 1, new ArrayList<String>(), s); return res; } public void search(List<String> res, boolean[][] isWord, boolean[] dp, int end, List<String> cur, String s) { if (end < 0) { StringBuilder sb = new StringBuilder(); for (int i = cur.size() - 1; i >= 0; i--) { sb.append(cur.get(i)); if (i != 0) { sb.append(" "); } } res.add(sb.toString()); } for (int i = end; i >= 0; i--) { if (isWord[i][end] && dp[i]) { cur.add(s.substring(i, end + 1)); search(res, isWord, dp, i - 1, cur, s); cur.remove(cur.size() - 1); } } } }
Wildcard Matching
通配符匹配
思路:属于双序列型动态规划,(注意题目中假设了只有p中含有通配符)。
状态:dp[i][j]表示s的前i个字符组成的前缀串与p的前j个字符组成的前缀串是否匹配。
方程:判断两个字符串是否匹配,有两种情况:
1 p的最后一个字母不为'*',则要求最后一个字母匹配(相等或其中一个字母为'?'),且前i-1字串与前j-1字串匹配;
2 p的最后一个字母为'*',如果它匹配s的最后一个空字符串,则等价于看dp[i][j-1],如果它匹配s末尾某个非空字符串,则等价于看dp[i-1][j]。
初始化:初始化第0行第0列;
结果:dp[slen][plen]。
1 public class Solution { 2 public boolean isMatch(String s, String p) { 3 int m = s.length(), n = p.length(); 4 char[] ws = s.toCharArray(); 5 char[] wp = p.toCharArray(); 6 boolean[][] dp = new boolean[m+1][n+1]; 7 dp[0][0] = true; 8 for (int j = 1; j <= n; j++) { 9 dp[0][j] = dp[0][j-1] && wp[j-1] == '*'; 10 } 11 for (int i = 1; i <= m; i++) { 12 dp[i][0] = false; 13 } 14 for (int i = 1; i <= m; i++) { 15 for (int j = 1; j <= n; j++) { 16 if (wp[j-1] == '?' || ws[i-1] == wp[j-1]) { 17 dp[i][j] = dp[i-1][j-1]; 18 } 19 else if (wp[j-1] == '*') { 20 dp[i][j] = dp[i-1][j] || dp[i][j-1]; 21 } 22 } 23 } 24 return dp[m][n]; 25 } 26 }