动态规划原理
基本思想: 问题的最优解如果可以由子问题的最优解推导得到,则可以先求解子问题的最优解,在构造原问题的最优解;若子问题有较多的重复出现 ,则可以自底向上 从最终子问题向原问题逐步求解。
使用条件:可分为多个相关子问题,子问题的解被重复使用
Optimal substructure(优化子结构):
一个问题的优化解包含了子问题的优化解
缩小子问题集合,只需那些优化问题中包含的子问题,降低实现复杂性
我们可以自下而上的
Subteties(重叠子问题):在问题的求解过程中,很多子问题的解将被多次使用。
动态规划算法的设计步骤:
分析优化解的结构
递归地定义最优解的代价
自底向上地计算优化解的代价保存之,并获取构造最优解的信息
根据构造最优解的信息构造优化解
动态规划特点:
把原始问题划分成一系列子问题;
求解每个子问题仅一次,并将其结果保存在一个表中,以后用到时直接存取,不重复计算,节省计算时间
自底向上地计算。
整体问题最优解取决于子问题的最优解(状态转移方程)(将子问题称为状态,最终状态的求解归结为其他状态的求解)
斐波那契数列 定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。
第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。
考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public int climbStairs (int n) { if (n <= 2 ) { return n; } int pre2 = 1 , pre1 = 2 ; for (int i = 2 ; i < n; i++) { int cur = pre1 + pre2; pre2 = pre1; pre1 = cur; } return pre1; } }
定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。
1 2 3 4 5 6 7 8 9 10 11 class Solution { public int rob (int [] nums) { int pre2 = 0 , pre1 = 0 ; for (int i = 0 ; i < nums.length; i++) { int cur = Math.max(pre2 + nums[i], pre1); pre2 = pre1; pre1 = cur; } return pre1; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution { public int rob (int [] nums) { int n = nums.length; if (nums == null || n == 0 ) { return 0 ; } if (n == 1 ) { return nums[0 ]; } return Math.max(robnat(nums, 0 , n - 2 ), robnat(nums, 1 , n - 1 )); } private int robnat (int [] nums, int first, int last) { int pre2 = 0 , pre1 = 0 ; for (int i = first; i <= last; i++) { int cur = Math.max(pre1, pre2 + nums[i]); pre2 = pre1; pre1 = cur; } return pre1; } }
母牛生产 题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。
第i年成熟的牛的数量:
因为牛都不死,所以肯定包含去年的成熟牛的数量,即dp[i-1];同时所有成熟的牛都会生一头新的牛,那么还要加上n-3年前的所有成熟牛,其间出生的牛都还没成熟。
矩阵路径 dp[j] = min(dp[j],dp[j-1])
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution { public int minPathSum (int [][] grid) { if (grid == null || grid.length == 0 ) { return 0 ; } int m = grid.length, n = grid[0 ].length; int [] dp = new int [n]; for (int i = 0 ; i < m; i++) { for (int j = 0 ; j < n; j++) { if (j == 0 ) { dp[j] = dp[j]; } else if (i == 0 ) { dp[j] = dp[j - 1 ]; } else { dp[j] = Math.min(dp[j - 1 ], dp[j]); } dp[j] += grid[i][j]; } } return dp[n - 1 ]; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ]; } }
自底向上 和旁边的比。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { /** * dp[i]表示包含triangle[i]层的最小路径和 * @param triangle * @return */ public int minimumTotal(List<List<Integer>> triangle) { if (triangle == null || triangle.size() == 0) { return 0; } int[] dp = new int[triangle.size() + 1]; for (int i = triangle.size() - 1; i >= 0; i--) { List<Integer> curTr = triangle.get(i); for (int j = 0; j < curTr.size(); j++) { dp[j] = Math.min(dp[j], dp[j + 1]) + curTr.get(j); System.out.println("i:"+i+" j:"+j+" "+dp[j]); } } return dp[0]; } }
数组区间 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class NumArray { private int sums[]; public NumArray (int [] nums) { sums = new int [nums.length + 1 ]; for (int i = 1 ; i <= nums.length; i++) { sums[i] = sums[i - 1 ] + nums[i - 1 ]; } } public int sumRange (int i, int j) { return sums[j + 1 ] - sums[i]; } }
dp[i] 表示以 A[i] 为结尾的等差递增子区间的个数。
当 A[i] - A[i-1] == A[i-1] - A[i-2],那么 [A[i-2], A[i-1], A[i]] 构成一个等差递增子区间。而且在以 A[i-1] 为结尾的递增子区间的后面再加上一个 A[i],一样可以构成新的递增子区间。
综上,在 A[i] - A[i-1] == A[i-1] - A[i-2] 时,dp[i] = dp[i-1] + 1。
因为递增子区间不一定以最后一个元素为结尾,可以是任意一个元素结尾,因此需要返回 dp 数组累加的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public int numberOfArithmeticSlices (int [] A) { if (A == null || A.length == 0 ) { return 0 ; } int n = A.length; int [] dp = new int [n]; for (int i = 2 ; i < n; i++) { if (A[i] - A[i - 1 ] == A[i - 1 ] - A[i - 2 ]) { dp[i] = dp[i - 1 ] + 1 ; } } int total = 0 ; for (int cnt : dp) { total += cnt; } return total; } }
最长递增子序列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution { public int lengthOfLIS (int [] nums) { if (nums.length == 0 || nums == null ) { return 0 ; } int n = nums.length; int [] dp = new int [n]; for (int i = 0 ; i < n; i++) { int max = 1 ; for (int j = 0 ; j < i; j++) { if (nums[i] > nums[j]) { max = Math.max(max, dp[j] + 1 ); } } dp[i] = max; } int ret = 0 ; for (int num : dp) { ret = Math.max(ret, num); } return ret; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution { public int findLongestChain (int [][] pairs) { if (pairs == null || pairs.length == 0 ) { return 0 ; } Arrays.sort(pairs, (a, b) -> (a[0 ] - b[0 ])); int n = pairs.length; int [] dp = new int [n]; Arrays.fill(dp, 1 ); for (int i = 1 ; i < n; i++) { for (int j = 0 ; j < i; j++) { if (pairs[i][0 ] > pairs[j][1 ]) { dp[i] = dp[j] + 1 ; } } } int ret = 0 ; for (int num : dp) { ret = Math.max(num, ret); } return ret; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public int wiggleMaxLength (int [] nums) { if (nums == null || nums.length == 0 ) { return 0 ; } int up = 1 ; int down = 1 ; for (int i = 1 ; i < nums.length; i++) { if (nums[i] > nums[i - 1 ]) { up = down + 1 ; } else if (nums[i] < nums[i - 1 ]) { down = up + 1 ; } } return Math.max(up, down); } }
最长公共子序列 dp[i][j]:表示在字符串text1[0…i]中和字符串text2[0…j]中最长公共子序列的长度为dp[i][j]。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { 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][j - 1 ], dp[i - 1 ][j]); } } } return dp[n1][n2]; } }
0-1 背包 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution { public boolean canPartition (int [] nums) { int sum = 0 ; for (int num : nums) { sum += num; } if (sum % 2 != 0 ) { return false ; } int w = sum / 2 ; boolean [] dp = new boolean [w + 1 ]; dp[0 ] = true ; for (int num : nums) { for (int i = w; i >= num; i--) { dp[i] = dp[i] || dp[i - num]; } } return dp[w]; } }
找到nums一个正子集和一个负子集,使得总和等于target
我们假设P是正子集,N是负子集 例如: 假设nums = [1, 2, 3, 4, 5],target = 3,一个可能的解决方案是+1-2+3-4+5 = 3 这里正子集P = [1, 3, 5]和负子集N = [2, 4]
那么让我们看看如何将其转换为子集求和问题:
1 2 3 sum(P) - sum(N) = target sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N) 2 * sum(P) = target + sum(nums)
因此,原来的问题已转化为一个求子集的和问题: 找到nums的一个子集 P,使得sum(P) = (target + sum(nums)) / 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { public int findTargetSumWays (int [] nums, int S) { int sum = 0 ; for (int num : nums) { sum += num; } if ((S + sum) % 2 != 0 || sum < S) { return 0 ; } int W = (sum + S) / 2 ; int [] dp = new int [W + 1 ]; dp[0 ] = 1 ; for (int num : nums) { for (int i = W; i >= num; i--) { dp[i] = dp[i] + dp[i - num]; } } return dp[W]; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution { public int findMaxForm (String[] strs, int m, int n) { if (strs == null || strs.length == 0 ) { return 0 ; } int [][] dp = new int [m + 1 ][n + 1 ]; for (String s : strs) { int ones = 0 , zeros = 0 ; for (char c : s.toCharArray()) { if (c == '0' ) { zeros++; } else { ones++; } } for (int i = m; i >= zeros; i--) { for (int j = n; j >= ones; j--) { dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1 ); } } } return dp[m][n]; } }
dp[i]表示金额为i需要最少的金币多少,
对于任意金额j,dp[j] = min(dp[j],dp[j-coin]+1),如果j-coin存在的话.
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 class Solution { public int coinChange (int [] coins, int amount) { int [] dp = new int [amount + 1 ]; for (int coin : coins) { for (int i = coin; i <= amount; i++) { if (i == coin) { dp[i] = 1 ; } else if (dp[i] == 0 && dp[i - coin] != 0 ) { dp[i] = dp[i - coin] + 1 ; } else if (dp[i - coin] != 0 ) { dp[i] = Math.min(dp[i], dp[i - coin] + 1 ); } } } if (dp[amount] == 0 ) { if (amount == 0 ) { return 0 ; } else { return -1 ; } } else { return dp[amount]; } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public int change (int amount, int [] coins) { if (coins == null ) { 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 - coin]; } } return dp[amount]; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public boolean wordBreak (String s, List<String> wordDict) { int n = s.length(); boolean [] dp = new boolean [n + 1 ]; dp[0 ] = true ; for (int i = 1 ; i <= n; i++) { for (String word : wordDict) { int len = word.length(); if (len <= i && word.equals(s.substring(i - len, i))) { dp[i] = dp[i] || dp[i - len]; } } } return dp[n]; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { public int combinationSum4 (int [] nums, int target) { if (nums.length == 0 || nums == null ) { return 0 ; } int [] maximum = new int [target + 1 ]; maximum[0 ] = 1 ; Arrays.sort(nums); for (int i = 1 ; i <= target; i++) { for (int num : nums) { if (num <= i) { maximum[i] = maximum[i] + maximum[i - num]; } else { continue ; } } } return maximum[target]; } }
股票交易
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution { public int maxProfit (int [] prices) { if (prices.length == 0 ) { return 0 ; } int n = prices.length; int [][] f = new int [n][3 ]; f[0 ][0 ] = -prices[0 ]; for (int i = 1 ; i < n; i++) { f[i][0 ] = Math.max(f[i - 1 ][0 ], f[i - 1 ][2 ] - prices[i]); f[i][1 ] = f[i - 1 ][0 ] + prices[i]; f[i][2 ] = Math.max(f[i - 1 ][1 ], f[i - 1 ][2 ]); } return Math.max(f[n - 1 ][1 ], f[n - 1 ][2 ]); } }
buy[i]:第i天买股票时的最大收益
sell[i]:第i天卖股票时的最大收益
s1[i]:第i天不操作且上一个操作是买时的最大收益
s2[i]:第i天不操作且上一个操作是卖时的最大收益
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public int maxProfit (int [] prices, int fee) { int n = prices.length; int [] buy = new int [n]; int [] sell = new int [n]; int [] s1 = new int [n]; int [] s2 = new int [n]; buy[0 ] = s1[0 ] = -prices[0 ]; sell[0 ] = s2[0 ] = 0 ; for (int i = 1 ; i < n; i++) { buy[i] = Math.max(sell[i - 1 ], s2[i - 1 ]) - prices[i]; sell[i] = Math.max(buy[i - 1 ], s1[i - 1 ]) - fee + prices[i]; s1[i] = Math.max(buy[i - 1 ], s1[i - 1 ]); s2[i] = Math.max(sell[i - 1 ], s2[i - 1 ]); } return Math.max(sell[n - 1 ], s2[n - 1 ]); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution { public int maxProfit (int [] prices) { int firstBuy = Integer.MIN_VALUE, firstSell = 0 ; int secondBuy = Integer.MIN_VALUE, secondSell = 0 ; for (int price : prices) { firstBuy = Math.max(firstBuy, -price); firstSell = Math.max(firstSell, firstBuy + price); secondBuy = Math.max(secondBuy, firstSell - price); secondSell = Math.max(secondSell, secondBuy + price); } return secondSell; } }
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 class Solution { public int maxProfit (int k, int [] prices) { if (k < 1 ) { return 0 ; } int n = prices.length; if (k >= n / 2 ) { int maxProfit = 0 ; for (int i = 1 ; i < n; i++) { if (prices[i] > prices[i - 1 ]) { maxProfit += prices[i] - prices[i - 1 ]; } } return maxProfit; } int [][] maxProfit = new int [k][2 ]; for (int i = 0 ; i < k; i++) { maxProfit[i][0 ] = Integer.MIN_VALUE; } for (int price : prices) { maxProfit[0 ][0 ] = Math.max(maxProfit[0 ][0 ], -price); maxProfit[0 ][1 ] = Math.max(maxProfit[0 ][1 ], maxProfit[0 ][0 ] + price); for (int i = 1 ; i < k; i++) { maxProfit[i][0 ] = Math.max(maxProfit[i][0 ], maxProfit[i - 1 ][1 ] - price); maxProfit[i][1 ] = Math.max(maxProfit[i][1 ], maxProfit[i][0 ] + price); } } return maxProfit[k - 1 ][1 ]; } }
字符串编辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 = 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 ] + 1 ; } else { dp[i][j] = Math.max(dp[i][j - 1 ], dp[i - 1 ][j]); } } } return m + n - 2 * dp[m][n]; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 <= m; i++) { dp[i][0 ] = i; } for (int i = 0 ; i <= n; i++) { dp[0 ][i] = 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 - 1 ], 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 21 22 class Solution { public int minSteps (int n) { int [] dp = new int [n + 1 ]; for (int i = 2 ; i <= n; i++) { dp[i] = i; for (int j = 2 ; j <= Math.sqrt(i); j++) { if (i % j == 0 ) { dp[i] = dp[j] + dp[i / j]; break ; } } } return dp[n]; } }