动态规划小结
这类题是真的多,这里总结几个大类,还有一些小类只能靠多刷题积累经验了,做过了可能就会,没遇到过可能就真的是想不出来。
动态规划的题一般来说问得是什么设什么dp[i]就好,如果从问题中看不出来设什么那就需要将问题进行转换了,这种题一般也比较难想。
真的是太多了,少贴几个,整理有点头皮发麻。
一、斐波那切数列
这一类是比较简单和基础的一类了。
状态转方程基本是:dp[i] = dp[i-1] + dp [i-2] i>=2 这个模型了。
思路:设dp[i]为爬到第i阶有多少种爬法。到第i阶可以从第i-1阶爬一步也可以从第i-2阶爬两步。
因此状态转换方程:dp[i] = dp[i-1] + dp[i-2]
class Solution { public int climbStairs(int n) { int dp_i1 = 1; int dp_i2 = 2; int dp_i = 0; if(n == 1) return 1; if(n == 2) return 2; for(int i = 3; i <= n ; i++) { dp_i = dp_i1 + dp_i2; dp_i1 = dp_i2; dp_i2 = dp_i; } return dp_i; } }
题目:有N个信和信封,它们被打乱,求错误装信方式的数量。
思路:设dp[n]为n封信的错误装信方式数量。
情况①第n封信错误的装在了1~n-1中的k位置,而k位置的信又正好装在n位置上,则有(n-1)*dp[n-2]
情况②第n封信错误的装在了1~n-1中的k位置,但是k位置的信不可以装在n位置上,则有(n-1)*dp[n-1]
状态转换方程:dp[n] = (n-1)*(dp[n-1] + dp[n-2])
二、矩阵路径
这类题目和走台阶的比较类似
设dp[i][j]为从开始点走到(i,j)点有几种走法。
情况①:边界位置只能够从左或者上其中一个方向走来来,故有dp[i][j] = 1;
情况②:中间位置可以从左或者上两个方向走来,故有dp[i][j] = dp[i-1][j] + dp[i][j-1];
class Solution { public int uniquePaths(int m, int n) { int[][] dp = new int[m][n]; for(int i = 0; i < m; i++) { for(int j = 0; j < n; j++) { if(i == 0 || j == 0) { dp[i][j] = 1; }else { dp[i][j] = dp[i-1][j] + dp[i][j-1]; } } } return dp[m-1][n-1]; } }
class Solution { public int minPathSum(int[][] grid) { int[][] dp = new int[grid.length][grid[0].length]; for(int i = 0; i < grid.length; i++) { for(int j = 0; j < grid[0].length; j++) { if(i == 0 && j == 0) { dp[0][0] = grid[i][j]; continue; } if(i == 0) { dp[i][j] = dp[i][j-1] + grid[i][j]; continue; } if(j == 0) { dp[i][j] = dp[i-1][j] + grid[i][j]; continue; } dp[i][j] = Math.min(dp[i][j-1],dp[i-1][j]) + grid[i][j]; } } return dp[grid.length-1][grid[0].length-1]; } }
三、各种序列问题
碰到序列问题首选动态规划,动态规划解决序列问题简直不要太好用。
设dp[i]为以i为结尾的等差子数组个数
if(A[i] - A[i-1] == A[i-1] - A[i-2]) dp[i] = dp[i-1] + 1;
如果i能够加到以i-1为等差子数组的序列中,则只是比前者多一个长度+1的情况
class Solution { //动态规划定义dp_i为以i为结尾的等差数列个数 private int sum = 0; public int numberOfArithmeticSlices(int[] A) { count(A,0); return sum; } //递归计算以i为开始的等差数列 private int count(int[] A, int i) { if(A.length - i < 3) return 0; else { int count = 0; if(A[i] - A[i+1] == A[i+1] - A[i+2]) { count = 1 + count(A,i+1); sum = sum + count; }else { count(A,i+1); } return count; } } }
这个题和上一个比较相似,但是处理边界有点棘手。
设dp[i]为以下标i结尾的字符串的解码方式
s[i]同s[i-1]组不成26以内的数字dp[i] = dp[i-1]
s[i]同s[i-1]可以组成26以内的数字dp[i] = dp[i-1] + dp[i-2](好像斐波那契哦)
class Solution { public int numDecodings(String s) { if (s == null || s.length() == 0) { return 0; } int n = s.length(); int[] dp = new int[n + 1]; dp[0] = 1; dp[1] = s.charAt(0) == '0' ? 0 : 1; for (int i = 2; i <= n; i++) { int one = Integer.valueOf(s.substring(i - 1, i)); if (one != 0) { dp[i] += dp[i - 1]; } if (s.charAt(i - 2) == '0') { continue; } int two = Integer.valueOf(s.substring(i - 2, i)); if (two <= 26) { dp[i] += dp[i - 2]; } } return dp[n]; } }
最长递增子序列
class Solution { public int lengthOfLIS(int[] nums) { if(nums.length <= 1) return nums.length; int dp[] = new int[nums.length]; Arrays.fill(dp,1); int ans = 0; for(int i = 1; i < nums.length; i++) { for(int j = 0; j < i; j++) { if(nums[j] < nums[i]) dp[i] = Math.max(dp[i],dp[j]+1); } ans = Math.max(ans,dp[i]); } return ans; } }
最长公共子序列
设dp[i][j]为以i,j为结尾的字符串的最长公共子序列。
情况①:Si = Sj dp[i][j] = dp[i-1][j-1] + 1;
情况②:Si != Sj dp[i][j] = max{dp[i-1][j], dp[i][j-1]} 要么i不在公共子序列中,要么j不在公共子序列中
class Solution { public int minDistance(String word1, String word2) { int l1 = word1.length(); int l2 = word2.length(); int[][] dp = new int[l1+1][l2+1]; for(int i = 1; i <= l1; i++) dp[i][0] = i; for(int i = 1; i <= l2; i++) dp[0][i] = i; for(int i = 1; i <= l1; i++) { for(int j = 1; j <= l2; 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]+1,dp[i][j-1]+1); } } return dp[l1][l2]; } }
注意与最长公共子字符串的区分
设dp[i][j]为以i,j为结尾的最长公共子串长度
情况①:Si = Sj dp[i][j] = dp[i-1][j-1] + 1;
情况②:Si != Sj dp[i][j] = 0 说明不存在以i,j为结尾的公共子串
ans = max{dp[0][0] ~ dp[i][j]}
class Solution { public int findLength(int[] A, int[] B) { int m = A.length; int n = B.length; int max = 0; int[][] dp = new int[m+1][n+1]; for(int i = 1; i <= m; i++) { for(int j = 1; j <= n; j++) { if(A[i-1] == B[j-1]) dp[i][j] = dp[i-1][j-1] + 1; else dp[i][j] = 0; max = Math.max(max,dp[i][j]); } } return max; } }
四、整数分割
class Solution { public int integerBreak(int n) { int[] dp = new int[n+1]; dp[2] = 1; for(int i = 3; i <= n; i++) { int max = 0; for(int j = 1; j < i - 1 ; j++) { int temp = Math.max(j*(i-j),j*dp[i-j]); if(temp > max) max = temp; } dp[i] = max; } return dp[n]; } }
class Solution { public int numSquares(int n) { int[] dp = new int[n+1]; dp[0] = 0; dp[1] = 1; for(int i = 2; i <= n; i++) { int m = (int)Math.pow(i,0.5); int min = Integer.MAX_VALUE; for(int j = 1; j <= m; j++) { int temp = i - j*j; if(dp[temp] < min) min = dp[temp]; } dp[i] = min + 1; } return dp[n]; } }
五、背包问题
(1)0/1背包
问题:描述有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
设dp[i][j]在j的空间里装前i个物品所达到的最大价值。
情况①:不装第i件物品,dp[i][j] = dp[i-1][j]
情况②:装第i件物品,那么就需要付出相应体积的代价 dp[i][j] = dp[i-1][j - w] + v
综上 dp[i][j] = max{dp[i-1][j], dp[i-1][j - w] + v}
这里有个需要特别注意的地方,我们这样去设置其实是默认了先装第1,2,3 ....i件物品,即装入物品有序。
还是老样子问什么设什么就好
设dp[i][j]装前i件物品在j的空间里是否能恰好装满。
dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i]]
class Solution { public boolean canPartition(int[] nums) { int sum = 0; for(int temp : nums) { sum = sum + temp; } int c = 0; if(sum % 2 == 1) return false; else c = sum/2; boolean[] dp = new boolean[c+1]; dp[0] = true; for(int i = 0; i < nums.length; i++) { for(int j = c; j >= nums[i]; j--) { dp[j] = dp[j] || dp[j-nums[i]]; } } return dp[c]; } }
Sum正 + Sum负 = S
Sum + Sum正 + Sum负 = S + Sum
2Sum正 = S + Sum
class Solution { public int findTargetSumWays(int[] nums, int S) { int sum = 0; for(int num : nums) { sum+=num; } if((S+sum)%2==1 || S > sum) return 0; int target = (S+sum)/2; int[][] dp = new int[nums.length+1][target+1]; dp[0][0] = 1; for(int i = 1; i < nums.length+1; i++) { dp[i][0] = 1; for(int j = 1; j < target+1; j++) { if(j >= nums[i-1]) { dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]]; }else { dp[i][j] = dp[i-1][j]; } } } return dp[nums.length][target]; } }
(2)多维背包
多维背包问题不但有体积的限制而且还有重量的限制,解决问题的方式同0/1背包只是dp数组多加一维罢了
class Solution { public int findMaxForm(String[] strs, int m, int n) { if(strs.length == 0) return 0; if(m < 0 || n < 0) return 0; int[][] dp = new int[m+1][n+1]; for(String str : strs) { for(int i = m; i >= count0(str); i--) { for(int j = n; j >= count1(str); j--) { dp[i][j] = Math.max(dp[i][j],dp[i-count0(str)][j-count1(str)] + 1); } } } return dp[m][n]; } private int count0(String s) { int count = 0; for(int i = 0; i < s.length(); i++) { if(s.charAt(i) == '0') count++; } return count; } private int count1(String s) { int count = 0; for(int i = 0; i < s.length(); i++) { if(s.charAt(i) == '1') count++; } return count; } }
(3)完全背包问题
完全背包问题的物品可以重复,只要稍微改一下0/1背包即可,但是注意这样装入仍然是有序的
dp[i][j] = max{dp[i-1][j], dp[i][j - w] + v}
我们改了一下方程,即表示装过i之后还可以装i。就是把0/1背包的反向遍历背包空间改成正向遍历
class Solution { public int coinChange(int[] coins, int amount) { int[] dp = new int[amount+1]; Arrays.fill(dp,amount+1); dp[0] = 0; for(int coin : coins) { for(int j = coin; j <= amount; j++) { dp[j] = Math.min(dp[j],dp[j-coin]+1); } } return dp[amount] == amount+1 ? -1 : dp[amount]; } }
class Solution { public int change(int amount, int[] coins) { if(amount < 0) return 0; int[] dp = new int[amount+1]; dp[0] = 1; for(int coin : coins) { for(int i = coin; i <= amount; i++) { dp[i] = dp[i] + dp[i-coin]; } } return dp[amount]; } }
注意观察这道题和上题的区别,这道题包含重复的解,而上一题不包含重复解,我们上面说方法的装入是有序的,所以是不包含重复解的。
这里我们来介绍另一种方式。
f(n) = ∑f(n - nums[i]) n ≥ nums[i]
class Solution { public int combinationSum4(int[] nums, int target) { if(target < 0) return 0; int[] dp = new int[target+1]; dp[0] = 1; for(int i = 1; i <= target; i++) { int sum = 0; for(int num : nums) { if(num <= i) { sum = sum + dp[i-num]; } } dp[i] = sum; } return dp[target]; } }
这题同样要求装入的方式无序
class Solution { public boolean wordBreak(String s, List<String> wordDict) { boolean[] dp = new boolean[s.length()+1]; dp[0] = true; for(int i = 1; i <= s.length(); i++) { for(String word : wordDict) { if(i>=word.length()) if(s.substring(i-word.length(),i).equals(word)) dp[i] = dp[i] || dp[i-word.length()]; } } return dp[s.length()]; } }
六、强盗问题
强盗问题也是一类0/1抉择问题
设dp[i][0]表示第i间不偷的最大利益,则有 dp[i][0] = max{dp[i-1][1], dp[i-1][0]}
设dp[i][1]表示第i间偷的最大利益,则有 dp[i][1] = dp[i-1][0] + nums[i]
class Solution { public int rob(int[] nums) { if(nums.length == 0) return 0; if(nums.length == 1) return nums[0]; int dp_i_0 = 0; int dp_i_1 = nums[0]; int predp_i_0 = dp_i_0; int predp_i_1 = dp_i_1; for(int i = 1; i < nums.length; i++) { dp_i_0 = Math.max(predp_i_0,predp_i_1); dp_i_1 = predp_i_0 + nums[i]; predp_i_0 = dp_i_0; predp_i_1 = dp_i_1; } return Math.max(dp_i_1,dp_i_0); } }
开头和结尾肯定有一家不能偷,我们假设开头不能偷或者结尾不能偷然后就变成了两个线性的问题。
class Solution { public int rob(int[] nums) { if(nums.length == 0) return 0; if(nums.length == 1) return nums[0]; if(nums.length == 2) return Math.max(nums[0],nums[1]); return Math.max(helprob(nums,0,nums.length-2),helprob(nums,1,nums.length-1)); } private int helprob(int[] nums, int start, int end) { int length = end - start + 1; if(length == 0) return 0; if(length == 1) return nums[start]; int dp_i_0 = 0; int predp_i_0 = dp_i_0; int dp_i_1 = nums[start]; int predp_i_1 = dp_i_1; for(int i = start + 1; i <= end; i++) { dp_i_0 = Math.max(predp_i_0,predp_i_1); dp_i_1 = predp_i_0 + nums[i]; predp_i_0 = dp_i_0; predp_i_1 = dp_i_1; } return Math.max(dp_i_0,dp_i_1); } }
七、股票问题
121. 买卖股票的最佳时机 (只能够做一次交易)
设dp[i][0]表示第i天不持有股票的最大利益,dp[i][1]表示第i天持有股票的最大利益。
dp[i][0] = max{dp[i-1][0], dp[i-1][1] + prices[i]}
dp[i][1] = max{dp[i-1][1], -prices[i]} (-prices[i]就表示今天是第一笔交易)
class Solution { public int maxProfit(int[] prices) { if(prices.length <= 1) return 0; int dp_i_0 = 0; int dp_i_1 = - prices[0]; int predp_i_0 = dp_i_0; int predp_i_1 = dp_i_1; for(int i = 1; i < prices.length; i++) { dp_i_0 = Math.max(predp_i_0,predp_i_1 + prices[i]); dp_i_1 = Math.max(predp_i_1,-prices[i]); predp_i_0 = dp_i_0; predp_i_1 = dp_i_1; } return dp_i_0; } }
122. 买卖股票的最佳时机 II(可以多次买卖)
dp[i][0] = max{dp[i-1][0], dp[i-1][1] + prices[i]}
dp[i][1] = max{dp[i-1][1], dp[i-1][0] - prices[i]}
class Solution { public int maxProfit(int[] prices) { if(prices.length <= 1) return 0; int dp_i_0 = 0; int dp_i_1 = -prices[0]; int predp_i_0 = dp_i_0; int predp_i_1 = dp_i_1; for(int i = 1; i < prices.length; i++) { dp_i_0 = Math.max(predp_i_0,predp_i_1 + prices[i]); dp_i_1 = Math.max(predp_i_1,predp_i_0 - prices[i]); predp_i_0 = dp_i_0; predp_i_1 = dp_i_1; } return dp_i_0; } }
123. 买卖股票的最佳时机 III(只能做两次交易)
这里我们多加一维变量来体现两次
dp[i][k][0] = max{dp[i-1][k][0], dp[i-1][k][1] + prices[i]}
dp[i][k][1] = max{dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]}
188. 买卖股票的最佳时机 IV (可以做K笔交易)
class Solution { public int maxProfit(int k, int[] prices) { int n = prices.length; if(n <= 1 || k <= 0) return 0; if(k >= 2*n) return f(prices); int[][][] dp = new int[n][k+1][2]; for(int i = 0; i < n; i++) { for(int j = 1; j <= k; j++) { if(i == 0) dp[i][j][1] = -prices[i]; else { dp[i][j][1] = Math.max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i]); dp[i][j][0] = Math.max(dp[i-1][j][0],dp[i-1][j][1]+prices[i]); } } } return dp[n-1][k][0]; } private int f(int[] prices) { int[][] dp = new int[prices.length+1][2]; dp[1][1] = -prices[0]; for(int i = 2; i < prices.length+1; i++) { dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i-1]); dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i-1]); } return dp[prices.length][0]; } }
dp[i][0] = max {dp[i-1][0], dp[i-1][1] + prices[i]}
dp[i][1] = max {dp[i-1][1], dp[i-2][0] - prices[i]} 表示今天不持股票可能是持有昨天持有的,也可能是昨天在冷冻期前天没有股票今天买入了
这是四种不同情况每一天只可能发生其中一种,不要去相互关联他们
class Solution { public int maxProfit(int[] prices) { int n = prices.length; if(n <= 1) return 0; int[] dp_0 = new int[n+1]; int[] dp_1 = new int[n+1]; dp_1[1] = -prices[0]; for(int i = 2; i <= n; i++) { dp_1[i] = Math.max(dp_1[i-1],dp_0[i-2]-prices[i-1]); dp_0[i] = Math.max(dp_0[i-1],dp_1[i-1]+prices[i-1]); } return dp_0[n]; } }
class Solution { public int maxProfit(int[] prices, int fee) { int n = prices.length; if(n<=1) return 0; int[] dp_0 = new int[n+1]; int[] dp_1 = new int[n+1]; dp_1[1] = -prices[0]; for(int i = 2; i <= n; i++) { dp_1[i] = Math.max(dp_1[i-1],dp_0[i-1]-prices[i-1]); dp_0[i] = Math.max(dp_0[i-1],dp_1[i-1]+prices[i-1]-fee); } return dp_0[n]; } }