在好早之前做过两篇dp的题目总结,那个时候并没有使用在线刷题工具,可能缺少被认证性。动态规划(Dynamic Progamming)是我最喜欢的分析方法之一,它拥有数学归纳法的优美,又可以解决计算机的问题。当然了,如果从理论角度去总结,我可能还不够格,但是从具体的问题去总结套路,在近两个月的刷题中也逐渐熟练。动态规划有一种,一眼看到问题一头雾水,想通了之后豁然开朗的感觉,就像证明归纳法k=n+1时候的样子。
借九章算法的候老师的总结,动态规划一般分为四个步骤:(1)状态;(2)状态转移方程;(3)初始条件;(4)终止条件
状态:对于一个可以迭代的问题,他的每一阶的迭代就是一个状态。比如 f(n)是一个问题为n阶的解,f(n)就是一个状态。
状态转移方程:状态是一个孤立的量,而状态之间的转移使得动态规划是有解的。状态转移方程类似是:f(n)=f(n-1)+f(n-2)这种形式的;之所以动态规划可以使用状态转移方程,是因为它具有重叠子问题并且构造最优子结构以达到全局最优解的特性。
初始条件:对于一个有状态的系统,它一定要有初态。
终止条件:同理。
动态规划还有一个特别明显的条件,就是自顶向下分析问题,自底向上构造解集。
收集了一波LC的DP题目,由浅入深。
70. Climbing Stairs
这个问题很经典,非常的简单。我们按部就班,首先找他的状态。
(1)假设它在每一个阶梯上的解集为f(n),这就是它的状态,f(n)表示的是楼梯为n的时候,它的ways的总数。
(2)这个时候要确定它的状态转移,题目意思是它到达一个阶梯n可以迈两步,也可以迈一步;所以,对于f(n)来说,它有可能来自f(n-1)也有可能来自f(n-2);因此f(n)=f(n-1)+f(n-2)。
(3)初始条件,f(0)=1,因为0级楼梯只有一种方法,f(1)=1,同理。
(4)终止条件为题目的要求解。
int calculator(int n){ if(n <= 1){ return 1; }else { return calculator(n-1)+calculator(n-2); } }
这样写是会超时的。该为循环计算就好了。
public int climbStairs(int n) { if(n <= 1){ return 1; }else { int an_2= 1; int an_1 = 1; int an = 0; for(int i=2;i<=n;i++){ an = an_2 + an_1; an_2 = an_1; an_1 = an; } return an; } }
198. House Robber
作为一个会DP的盗贼,你得在一排联排别墅中偷东西,并且价值最大,然后你还不能连续偷两家,因为会触发警报。
我们依然按部就班,从定义它的状态开始。
(1)这里的条件是联排别墅,那么就令f(n)表示n个别墅的时候,可以偷窃的最大价值。
(2)对于这个问题,因为它的限制是如果A[n]被抢了,那么A[n-1]必然是不能抢的。所以它的状态转移方程应该是f(n)=max{f(n-1),f(n-2)+A[n]},A[n]表示的是第n家的价值。
(3)初始条件f(0)=A[0],f(1)=max{A[0],A[1]}。
(4)所求。
/** * f(n) = max{f(n-1),f(n-2)+A[n]} * f(0) = A[0] * f(1) = max{f(0),A[1]} * */ public int rob1(int[] nums) { if(nums == null || nums.length == 0){ return 0; }else if(nums.length == 1){ return nums[0]; }else { int a1 = nums[0]; int a2 = Math.max(nums[1],nums[0]); int f = Math.max(a1,a2); for(int i=2;i){ f = Math.max(a1+nums[i],a2); a1 = a2; a2 = f; } return f; } }
53. Maximum Subarray
这道题是算法导论的第一个例题,也是遇到频率非常高的题目,名曰:最大字段和。股票涨跌,算出收益最大的区间,经典问题。
这题有好几种做法,这里只做dp的。
(1)状态,设g(n)为长度n的数组的最大字段和,f(n)为位置n存在的最大字段,g(n)=max{f(n)}。
(2)如果一个数组位置的值加上上一个状态,比这个位置的值还大,那么它们就是连续子段。所以一个状态等于f(n)=max{A[n],f(n-1)+A[n]}。
(3)f(0)=0。
(4)略。
/** * 设第n个数的最大子段和为f(n) * f[n] = max{d[0],...d[n]} * d[n] = max{A[n],A[n]+d(n-1)}*/ public int maxSubArray(int[] nums) { int f = nums[0]; int max = f; for(int i=1;i){ f = Math.max(nums[i],f + nums[i]); max = Math.max(f,max); } return max; }
120. Triangle
这也是一道痕经典的数塔问题,在第n层(n>=1)有n个元素,并且是二叉树的形式。d[n][m] 的左子树为 d[n+1][m] ,右子树为 d[n+1][m+1]。
这个题要从最底层不断的合并,最后才能确定最优解。
(1)令f(i,j)表示位置为i,j的元素的最小值
(2)易得,f(i,j)=min{f(i+1,j),f(i+1,j+1)}+A[i][j]
(3)初始值,f(i,j)为最底部的A[i][j]的边。
(4)f(0,0)为结果。
在这里我直接做了优化,使用了一维数组存储,空间复杂度为O(n)。
class Solution { public int minimumTotal(List> triangle) { if(triangle.isEmpty()){ return 0; } int[] countResult = new int[triangle.size()]; for(int i=0;i
){ countResult[i]=triangle.get(triangle.size()-1).get(i); } for(int i=triangle.size()-2;i>=0;i--){ for(int j=0;j ){ countResult[j] = Math.min(triangle.get(i).get(j)+countResult[j],triangle.get(i).get(j)+countResult[j+1]); } } return countResult[0]; } }
300. Longest Increasing Subsequence
最长上升子序列,新手噩梦。subsequence的意思,是非连续元素的子集。这里要计算的是最长的上升子序列,上升的意思是大于的数在后边。
在这里开始就不分步了,只总结关键的步骤。令 f(n)表示位置为n的最长字段,位置为n的意思的元素A[n]与之前的元素A[0:n-1]所组成的最长上升子序列的值,这表示题解要求的是max{f(n)}。
这个解法中,时间复杂度是O(n^2)。
class Solution { public int lengthOfLIS(int[] nums) { if(nums == null || nums.length == 0){ return 0; }else if(nums.length == 1){ return 1; } int[] record = new int[nums.length]; int len = 0; record[0] = 1; for(int i=1;i){ int cur = nums[i]; int max = 1; for(int j=i-1;j>=0;j--){ if(cur > nums[j] && record[j] + 1 > max){ max = record[j] + 1; } } record[i] = max; if(record[i] > len) len = record[i]; } return len; } }
题目中提示的O(nlogn)的解法,是维护一个最长的单调子序列,每次来一个元素就二分查找插入,最后这个容器中的size就是题解,从13ms优化到了1ms。
class Solution { public int lengthOfLIS(int[] nums) { if(nums == null){ return 0; }else if(nums.length <= 1){ return nums.length; } Liststack = new ArrayList<>(); stack.add(nums[0]); for(int i=1;i ){ if(nums[i] > stack.get(stack.size()-1)) { stack.add(nums[i]); continue; } int left = 0; int right = stack.size()-1; int mid = left + (right - left)/2;; while(left < right){ if(nums[i] > stack.get(mid)){ left = mid + 1; }else if(nums[i] < stack.get(mid)){ right = mid - 1; }else { break; } mid = left + (right - left)/2; } while(true){ if(mid + 1 < stack.size() && stack.get(mid + 1) == nums[i]){ mid ++; }else { break; } } if(stack.get(mid) < nums[i]) mid ++; stack.set(mid,nums[i]); } return stack.size(); } }
152. Maximum Product Subarray
一言以蔽之,最大字段积。
这里有坑,就是负数最小值在乘以一个负数的时候,会变成最大,所以需要计算两个轨迹,最大最小。
class Solution { public int maxProduct(int[] nums) { if(nums == null || nums.length == 0) { return 0; } int minArray = nums[0]; int maxArray = nums[0]; int result = nums[0]; for(int i=1;i){ int minI = nums[i]*minArray; int maxI = nums[i]*maxArray; minArray = Math.min(Math.min(minI,maxI),nums[i]); maxArray = Math.max(Math.max(minI,maxI),nums[i]); result = Math.max(result,maxArray); } return result; }
62. Unique Paths
这道题也是很简单的二维动规,一个位置只能往右或者往下,求左上角到右下角的unique path。每一个位置是一个状态,易得f(i,j)=f(i-1,j)+f(i,j-1),i>=1,j>=1。很容易优化为一维数组。
从最右下格子来看,对于每一个格子,它的来源只能是上方和左方,所以每个格子是f(i,j),他来自上方是f(i-1,j),来自左方是f(i,j-1)。
class Solution { public int uniquePaths(int m, int n) { if(m == 0 && n == 0){ return 0; }else if(n == 0 || m == 0){ return 1; } int[] calSet = new int[n]; calSet[0] = 1; for(int i=0;i){ for(int j=1;j ){ calSet[j] += calSet[j-1]; } } return calSet[n-1]; } }
63. Unique Paths II
这道题类似于数学题第二问一样,如果在某位置加上一个障碍,指的是任何路线经过这里都会变成0,边界条件很坑爹。
public class UniquePathsWithObstacles { /** * f(n)(m) = f(n-1)(m)+f(n)(m-1) n>=1,m>=1 when A[n][m] == 0 * f(n)(m) = 0 A[n][m] > 0 other */ public int uniquePathsWithObstacles(int[][] obstacleGrid) { if(obstacleGrid.length == 0 && obstacleGrid[0].length == 0){ return 0; } int[] calSet = new int[obstacleGrid[0].length]; if(obstacleGrid[0][0] == 0) calSet[0] = 1; for(int i=0;i){ for(int j=0;j ){ if(obstacleGrid[i][j] > 0){ calSet[j] = 0; continue; } if(j > 0){ calSet[j] += calSet[j-1]; } } } return calSet[obstacleGrid[0].length-1]; } }
322. Coin Change
找零问题,问如果有coins这些面额的硬币,如何对amount找零使得硬币数最小。
假设这个问题的解为f(n),它代表的是找零n所需要的最小硬币数。f(n)=min{f(n-A[i])+1},0<=i 对于amount=0,则需要0个硬币,所以f(0)=0。 假设A=[1,2,5],amount=11。 f(1)=f(1-1)+1;f(1-2)+1;f(1-5)+1,那些小于0的认为是没有解,所以略过。所以f(1)的硬币数目是1。 f(2)=f(2-1)+1;f(2-2)+1;f(2-5)+1,这时候f(2)是最小的,所以f(2)的硬币数目是1。 也就是说在找2块钱最少硬币的时候,需要对所有面额的硬币进行找零,选出最少的一个存储在2的位置,保证它是最优子结构。 依次迭代,最后得到amount=11的解。 这道抢劫题,是那道easy的升级题,它的不同是联排别墅是环形的,也就是第一个和最后一个是相邻的,变相的说抢第一个就不能抢最后一个了的意思。那么我们就计算两次,抢第一个和不抢第一个,用最大价值的。 这也是抢劫题的一个升级,就是联排别墅是树形的,直接连接的两个点不能抢,这题比较抽象。 我们使用后序遍历,每次返回一个长度为2的数组,一个数组位表示抢当前节点,一个数组位表示不抢当前节点。在遍历完成之后,比较两个元素取最大。这里注意,即使当前节点不抢,也要使用上级节点的两个值中的最大值,因为涉及到最大价值。 最大上升子序列的长度。 这题是hard难度,最初接触的时候毫无头绪,甚至乎连递归都写不出来。这里的难点是在于 *,这个字符,他表示0个或多个preceding字符。在没有它的时候,这个题目就很简单了。 只考虑 . 这个字符,那么每次模式串和目标串都会递进一个字符。而加入了*字符,那么当前可能一直匹配*,也可以跳过(零次匹配)。 递归的方法,速度比较慢,那自然有DP的解法了。令dp[i][j]代表text[0:i],pattern[0:j]的匹配状况。 如果当前不为*,那很简单,他们当前字符是否相等与上i-1,j-1位置的匹配就好。dp[i][j]=dp[i-1][j-1] 如果当前为*,那么它在当前去掉模式串的 X* 之后,字符是否匹配;例如 aabb 和 aab*,在最后位置的匹配应该是aabb 与 aa 的匹配基础上 再加上 b*,因为 b* 可以代表0次。dp[i][j] = dp[i][j-2] 如果当前为*,那么*的上一个字符与当前字符相等,那么它应该与去掉目标串当前字符的匹配一致;例如 aabb 和 aab*,它的匹配度应该与 aab 和 aab* 是一致的。dp[i][j] = dp[i-1][j] 带*的情况,只要满足一个就算是匹配。 给定一个字符串,只含有括号和反括号,确定能组成括号的最长字符串。最早,我使用stack计算最长的括号,复杂度也算是n不过有系数,大概是2n。 左括号入栈右括号出栈并且计算最大长度。这样就得到一个数组,就是括号长度的数组,这时候从最后往前计算,加起来算出链接起来最长的括号字符串。 对于一个长的括号字符串,它的形式可能是(),或者(()),也就是串行链接和嵌套。 如果是串行链接,那么很简单,闭合之后为上一个位置加2。dp[i]=dp[i-1]+2。 如果是嵌套链接,那么其实它可能是(()),或者((...)),从右边数过来第二个的长度加上左边第一个加上2。dp[i] = dp[i-1] + dp[i-dp[i-1]-1] + 2。 这道题其实比之前那到更为简单,*表示一个任意长的子段。 递归是超时的。 s1与s2交错字符串成s3。 解码游戏,*表示任意数字。 class Solution {
public int coinChange(int[] coins, int amount) {
if(amount < 0){
return -1;
}
int[] dynamicProgram = new int[amount + 1];
dynamicProgram[0] = 0;
for(int i=1;i<=amount;i++){
dynamicProgram[i] = Integer.MAX_VALUE;
for(int coin : coins){
if(i - coin >= 0 && dynamicProgram[i-coin] < Integer.MAX_VALUE){
if(dynamicProgram[i] > dynamicProgram[i-coin] + 1){
dynamicProgram[i] = dynamicProgram[i-coin] + 1;
}
}
}
}
if(dynamicProgram[amount] == Integer.MAX_VALUE){
dynamicProgram[amount] = -1;
}
return dynamicProgram[amount];
}
}
213. House Robber II
class Solution {
public int rob(int[] nums) {
if(nums == null || nums.length == 0){
return 0;
}else if(nums.length == 1){
return nums[0];
}else {
int a1 = nums[0];
int b1 = Math.max(nums[0],nums[1]);
int result1 = Math.max(a1,b1);
int a2 = 0;
int b2 = nums[1];
int result2 = Math.max(a2,b2);
for(int i=2;i
337. House Robber III
class Solution {
public int rob(TreeNode root) {
int[] answer = deepFirstSearch(root);
return Math.max(answer[0],answer[1]);
}
public int[] deepFirstSearch(TreeNode node){
if(node == null){
return new int[2];
}
int[] left = deepFirstSearch(node.left);
int[] right = deepFirstSearch(node.right);
int[] cur = new int[2];
cur[0] = left[1] + right[1] + node.val;
cur[1] = Math.max(left[0],left[1])+ Math.max(right[0],right[1]);
return cur;
}
}
673. Number of Longest Increasing Subsequence
public class NumberOfLongestIncreasingSubsequence {
public void test(){
int[] nums= {2,2,2,2,2,2};
System.out.println(findNumberOfLIS(nums));
}
/**
* status: let f[n] for i:0->n in A[i] the longest subsequence
* let sum[n] for i:0->n in A[i] the longest subsequence counting
* let j:i->n
* f[j]=max{f[i]+1} if(A[i]<=A[j])
* sum[j] = sum[j] + sum[i] when f[j] = f[i] + 1
* sum[j] = sum[i] when f[j] <= f[i]
* total = sum{sum[k]} if f[k] == longest
*/
public int findNumberOfLIS(int[] nums) {
if(nums.length == 0)
return 0;
int[] dp = new int[nums.length];
int[] sum = new int[nums.length];
Arrays.fill(dp,1);
Arrays.fill(sum,1);
dp[0] = 1;
int longest = 1;
for(int i=1;i
10. Regular Expression Matching
public boolean isMatch2(String text, String pattern) {
// when the Regular Expression is end,the text should be end
if(pattern.isEmpty())
return text.isEmpty();
// match the first character
boolean firstMatching = (!text.isEmpty() && text.charAt(0) == pattern.charAt(0) || pattern.charAt(0) == '.');
// current match and skip current character match
return firstMatching && isMatch(text.substring(1),pattern.substring(1));
}
public boolean isMatch(String text, String pattern) {
// when the Regular Expression is end,the text should be end
if(pattern.isEmpty())
return text.isEmpty();
// match the first character
boolean firstMatching = (!text.isEmpty() && text.charAt(0) == pattern.charAt(0) || pattern.charAt(0) == '.');
if(pattern.length() > 1 && pattern.charAt(1) == '*'){
return isMatch(text,pattern.substring(2)) || (firstMatching && isMatch(text.substring(1),pattern));
}else {
// current match and skip current character match
return firstMatching && isMatch(text.substring(1),pattern.substring(1));
}
}
/**
*
* Dynamic Programming
* let f[i,j] represent A[0:i],B[0:j] match
*
* if j == '*'
* f[i][j]=f[i][j-2]
* f[i][j]=(i==j-1 || j-1=='.')|f[i-1][j]
* else if (i==j || j=='.')
* f[i,j]=f[i-1,j-1]
*
*
*/
public boolean isMatch1(String text,String pattern){
boolean[][] dp = new boolean[text.length()+1][pattern.length()+1];
dp[0][0] = true;
for(int i=1;i
32. Longest Valid Parentheses
class Solution {
public int longestValidParentheses(String s) {
Stack
class Solution {
public int longestValidParentheses(String s) {
int[] dp = new int[s.length()];
int maxVal = 0;
for(int i=1;i
44. Wildcard Matching
public class WildcardMatching {
public void test(){
// true
System.out.println(isMatch("aa","a?"));
// false
System.out.println(isMatch("cb","?a"));
// false
System.out.println(isMatch("aa","a"));
// true
System.out.println(isMatch("aa","*"));
// true
System.out.println(isMatch("adceb","*a*b"));
// false
System.out.println(isMatch("acdcb","a*c?b"));
// false
System.out.println(isMatch("","?"));
// true
System.out.println(isMatch("","*"));
// true
System.out.println(isMatch("ho","ho**"));
}
public boolean isMatch1(String text, String pattern) {
if(pattern.isEmpty()){
return text.isEmpty();
}
boolean firstMatching = !text.isEmpty() && (text.charAt(0) == pattern.charAt(0)|| pattern.charAt(0) == '?');
if(pattern.charAt(0) == '*'){
if(!text.isEmpty() && pattern.length() > 1){
// repeat zero or more times
return isMatch(text.substring(1),pattern) || isMatch(text,pattern.substring(1));
}else {
if(!text.isEmpty()){
return isMatch(text.substring(1),pattern);
}else {
return isMatch(text,pattern.substring(1));
}
}
}else {
return firstMatching && isMatch(text.substring(1),pattern.substring(1));
}
}
/**
* * for zero or more sequence
* ? for single character
*
* Dynamic Programming
* let i for s[0:i] j for p[0:j]
* f[i,j] = f[i-1,j-1] when s[i]==p[j] or p[j] == '?'
* f[i,j] = f[i-1,j] or f[i,j-1] when p[j] == '*'
*/
public boolean isMatch(String text, String pattern){
boolean[][] dp = new boolean[text.length()+1][pattern.length()+1];
dp[0][0] = true;
for(int i=1;i
97. Interleaving String
public class InterleavingString {
public void test(){
// true
System.out.println(isInterleave("ab","ba","abba"));
// true
System.out.println(isInterleave("aabcc","dbbca","aadbbcbcac"));
// false
System.out.println(isInterleave("aabcc","dbbca","aadbbbaccc"));
// false
System.out.println(isInterleave("","","a"));
// true
System.out.println(isInterleave("","",""));
// false
System.out.println(isInterleave("a","","aa"));
// true
System.out.println(isInterleave("aabc","abad","aabadabc"));
}
/**
* s1,s2 construct s3
*
* i for s1[0:i],j for s2[0:j]
* i+j for s3
* dp[i][j] = (dp[i-1][j] when s3(i+j-1)==s1(i-1)) or (dp[i][j-1] when s3(i+j-1)==s2(j-1))
*/
public boolean isInterleave(String s1, String s2, String s3) {
if (s3.length() != s1.length() + s2.length()) {
return false;
}
boolean[][] dp = new boolean[s1.length()+1][s2.length()+1];
dp[0][0] = true;
for(int i=1;i<=s2.length();i++){
if(s3.charAt(i-1) == s2.charAt(i-1)){
dp[0][i] = dp[0][i-1];
}
}
for(int i=1;i<=s1.length();i++){
if(s3.charAt(i-1) == s1.charAt(i-1)){
dp[i][0] = dp[i-1][0];
}
}
for(int i=1;i<=s1.length();i++){
for(int j=1;j<=s2.length();j++){
if(s3.charAt(i+j-1) == s1.charAt(i-1)){
dp[i][j] = dp[i-1][j];
}
if(s3.charAt(i+j-1) == s2.charAt(j-1)){
dp[i][j] |= dp[i][j-1];
}
}
}
return dp[s1.length()][s2.length()];
}
}
639. Decode Ways II
class Solution {
/**
* 'A' -> 1
* 'B' -> 2
* ...
* 'Z' -> 26
*
* '*' for '1'-'9'
*
* .....'*'1
* .....1'*'
*
* Dynamic Programming
* let f[i] for s[0:i] type sum
*
* s[0] = 1
* if s[i] == '1'->'9'
* s[i] += s[i-1]
* if s[i-1:i] == '10'->'26'
* s[i] += s[i-2]
*
* if s[i] == '*'
* s[i] += 9*s[i-1]
* if s[i-1] == '1' and s[i] == '*'
* s[i] += 9*s[i-2]
* if s[i-1] == '2' and s[i] == '*'
* s[i] += 6*s[i-2]
*
* if s[i-1] == '*' and s[i] == '0'->'6'
* s[i] += 2*s[i-2]
* if s[i-1] == '*' and s[i] == '7'->'9'
* s[i] += s[i-2]
*
* // total type for this is 11-19 and 21-26
* if s[i-1] == '*' and s[i] == '*'
* s[i] += 15*s[i-2]
*/
public int numDecodings(String s) {
if(s == null || s.length() == 0){
return 0;
}
// fuck , why not return long
// gao zheme duo yaoerzi
// since the answer is very large you should return the output mod 109 + 7.
int M = 1000000007;
long[] dp = new long[s.length()+1];
dp[0] = 1;
dp[1] = s.charAt(0) == '*' ? 9 : s.charAt(0) == '0' ? 0 : 1;
for(int i=2;i<=s.length();i++){
char charactor = s.charAt(i-1);
char charactorPre = s.charAt(i-2);
if(charactor != '*' && charactorPre != '*'){
if(charactor >= '1' && charactor <= '9'){
dp[i] = (dp[i] + dp[i-1]) % M;
}
int val = Integer.parseInt(s.substring(i-2,i));
if(val >= 10 && val <= 26){
dp[i] = (dp[i] + dp[i-2]) % M;
}
}else if(charactorPre != '*'){
dp[i] = (dp[i] + 9*dp[i-1]) % M;
if(charactorPre == '1'){
dp[i] = (dp[i] + 9*dp[i-2]) % M;
}else if(charactorPre == '2'){
dp[i] = (dp[i] + 6*dp[i-2]) % M;
}
}else if(charactor != '*'){
if(charactor >= '1' && charactor <= '9'){
dp[i] = (dp[i] + dp[i-1]) % M;
}
if(charactor >= '0' && charactor <= '6'){
dp[i] = (dp[i] + 2*dp[i-2]) % M;
}
if(charactor >= '7' && charactor <= '9'){
dp[i] = (dp[i] + dp[i-2]) % M;
}
}else {
dp[i] = (dp[i] + 9*dp[i-1]+15*dp[i-2]) % M;
}
}
return (int)dp[s.length()];
}
}