動態規划的實質: 根據小問題的結果來判斷大問題的結果
- 記憶化搜索
- 避免中間重復的計算結果
什么時候使用動態規划:
- 求最大最小值
- 判斷是否可行
- 統計方案個數
什么時候不用動態規划:
- 求出所有具體的方案而非方案個數
- 輸入數據是一個集合而不是序列
- 暴力算法的復雜度已經是多項式級別
-
- 動態規划擅長優化指數級別復雜度(2^n, n!)到多項式級別復雜度(n^2, n^3)
- 不擅長優化n^3到n^2
動態規划4要素
- 狀態 存儲小規模問題的結果
- 方程 狀態之間的聯系,怎么通過小的狀態來算大的狀態
- 初始化 最極限的小狀態是什么,起點
- 答案 最大的那個狀態是什么,終點
面試中動態規划類型:
1. 坐標型動態規划
state:
f[x]表示我從起點走到坐標x....
f[x][y] 表示我從起點走到坐標x,y....
function: 研究走到x,y這個點之前的一步
initialize: 起點
answer:終點
1.1 Minimum Path Sum
Given a m x n grid filled with non-negative numbers, find a path from top left to bottom right which minimizes the sum of all numbers along its path.

1 public class Solution {
2 public int minPathSum(int[][] grid) {
3 if (grid == null || grid.length == 0 || grid[0].length == 0) {
4 return 0;
5 }
6
7 //initialize
8 int m = grid.length;
9 int n = grid[0].length;
10 int[][] sum = new int[m][n];
11 sum[0][0] = grid[0][0];
12
13 for (int i = 1; i < m; i++) {
14 sum[i][0] = grid[i][0] + sum[i - 1][0];
15 }
16
17 for (int i = 1; i < n; i++) {
18 sum[0][i] = grid[0][i] + sum[0][i - 1];
19 }
20
21 //dp
22 for (int i = 1; i < m; i++) {
23 for (int j = 1; j < n; j++) {
24 sum[i][j] = grid[i][j] + Math.min(sum[i - 1][j] , sum[i][j - 1]);
25 }
26 }
27 return sum[m - 1][n -1];
28 }
29 }
注意: 對於二維數組的判斷, a == NULL || a.length == 0 || a[0].length == 0
state: f[x][y] 從起點走到x,y的最小路徑
function: f[x][y] = min(f[x - 1][y], f[x][y - 1]) + A[x][y];
initialize: f[i][0] = sum(0,0~i,0)
f[0][i] = sum(0,0~0,i)
answer: f[m - 1][n - 1]
記住: 初始化一個二位的動態規划時,就去初始化第0行和第0列
1.2 Unique Paths
A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below).
The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked 'Finish' in the diagram below).
How many possible unique paths are there?

1 public class Solution { 2 public int uniquePaths(int m, int n) { 3 if (m == 0 || n == 0) { 4 return 0; 5 } 6 7 int[][] sum = new int[m][n]; 8 for (int i = 0; i < m; i++) { 9 sum[i][0] = 1; 10 } 11 for (int i = 0; i < n; i++) { 12 sum[0][i] = 1; 13 } 14 for (int i = 1; i < m; i++) { 15 for (int j = 1; j < n; j++) { 16 sum[i][j] = sum[i - 1][j] + sum[i][j - 1]; 17 } 18 } 19 return sum[m - 1][n - 1]; 20 } 21 }
state: f[x][y] 表示從起點到(x,y)的路徑數
function: f[x][y] = f[x - 1][y] + f[x][y - 1]
initialize: f[0][i] = 1; f[i][0] = 1;
answer: f[m - 1][n - 1]
1.3 Jump Game
最優算法:貪心法 時間復雜度為o(n)
次優算法:動態規划,時間復雜度為o(n^2)

1 public class Solution { 2 public boolean canJump(int[] A) { 3 // think it as merging n intervals 4 if (A == null || A.length == 0) { 5 return false; 6 } 7 int farthest = A[0]; 8 for (int i = 1; i < A.length; i++) { 9 if (i <= farthest && A[i] + i >= farthest) { 10 farthest = A[i] + i; 11 } 12 } 13 return farthest >= A.length - 1; 14 } 15 }

1 public class Solution { 2 public boolean canJump(int[] nums) { 3 // 會超時 4 int length = nums.length; 5 boolean[] canJump = new boolean[length]; 6 canJump[0] = true; 7 8 9 for (int i = 1; i < length; i++) { 10 for (int j = 0 ; j < i; j++) { 11 if (canJump[j] && j + nums[j] >= i) { 12 canJump[i] = true; 13 break; 14 } 15 } 16 } 17 return canJump[length - 1]; 18 } 19 }
1.4 Jump Game 2
最優算法:貪心法 時間復雜度為o(n)
次優算法:動態規划,時間復雜度為o(n^2)

1 public class Solution { 2 public int jump(int[] A) { 3 int[] steps = new int[A.length]; 4 5 steps[0] = 0; 6 for (int i = 1; i < A.length; i++) { 7 steps[i] = Integer.MAX_VALUE; 8 for (int j = 0; j < i; j++) { 9 if (steps[j] != Integer.MAX_VALUE && j + A[j] >= i) { 10 steps[i] = steps[j] + 1; 11 break; 12 } 13 } 14 } 15 16 return steps[A.length - 1]; 17 } 18 }

1 public class Solution { 2 public int jump(int[] nums) { 3 int minStep = 0; 4 int maxDis = 0; 5 int last = 0; 6 for (int i = 0; i < nums.length; i++) { 7 if (i > last) { 8 last = maxDis; 9 minStep++; 10 } 11 maxDis = Math.max(maxDis, nums[i] + i); 12 } 13 return minStep; 14 } 15 }
greedy 方法的核心思想是
3個變量 1. 當前第幾次跳 2 掃描到當前點並且在這次跳范圍內能達到的最遠距離 3 整體的能達到的最遠距離
掃一遍數組,找到第x次能跳的最大距離, 1. 第x+1個點 如果超出當前跳能達到的距離,那么更新跳的次數
2. 第x+1個點 如果沒超出當前能達到的距離(即當前點也是當前次數能跳到的),那么比較i+a[i]和currentMax的
大小,看看是否需要更新局部的最遠距離。
做一次新的跳躍后,把局部的最遠距離賦值給整體的最遠距離。
總之做法很牛逼。。。。
1.5 Longest Increasing Subsequence

1 public class Solution { 2 public int lengthOfLIS(int[] nums) { 3 if (nums == null || nums.length == 0) { 4 return 0; 5 } 6 int[] minNum = new int[nums.length]; 7 minNum[0] = 0; 8 int max = 0; 9 for (int i = 1; i < nums.length; i++) { 10 int j = i - 1; 11 while (j >= 0) { 12 if (nums[i] > nums[j]) { 13 minNum[i] = Math.max(minNum[j] + 1, minNum[i]); 14 } 15 j--; 16 } 17 } 18 Arrays.sort(minNum); 19 return minNum[nums.length - 1] + 1; 20 } 21 }
1.6 Maximal Square
二維坐標型動態規划
思路: 分析大問題的結果與小問題的相關性 f[i][j] 表示以i和j作為正方形的右下角可以擴展的最大邊長
eg: 1 1 1
1 1 1
1 1 1(traget)
traget 的值與3部分相關 1. 青色的正方形部分 f[i - 1][j - 1]
2. 紫色 target上面的數組 up[i][j - 1]即target上面的點 往上延伸能達到的最大長度
3. 橙色的target左邊的數組 left[i - 1][j]
如果 target == 1
f[i][j] = min (left[i - 1][j], up[i][j - 1], f[i - 1][j - 1]) + 1;
對於left和up數組 可以在intialization的時候用o(n^2)掃描整個圖形實現!
優化思路1:
因為
f[i - 1][j] = left[i - 1][j]
f[i][j - 1] = up[i][j - 1]
這樣 不需要額外的建立left和up數組
if (target == 1)
f[i][j] = min (f[i - 1][j - 1], f[i][j - 1],f[i - 1][j]) + 1;
優化思路2:
由於f[i][j]只和前3個結果相關
f[i - 1][j - 1] f[i][j - 1]
f[i - 1][j] f[i][j]
故只需要保留一個2行的數組!!!
列上不能優化,因為2重循環的時候 下列的時候依賴於上列的結果,上列的結果需要保存到計算下列的時候用。
只能在行上滾動,不能行列同時滾動!!!
--------------------
state: f[i][j] 表示以i和j作為正方形右下角可以擴展的最大邊長
function:
if matrix[i][j] == 1
f[i % 2][j] = min(f[(i - 1) % 2 ][j], f[(i - 1) % 2][j - 1], f[i%2][j - 1]) + 1;
initialization:
f[i%2][0] = matrix[i][0]
f[0][j] = matrix[0][j]
answer:
max{f[i%2][j]}

1 public class Solution { 2 public int maximalSquare(char[][] matrix) { 3 4 int result = 0; 5 int m = matrix.length; 6 if (m == 0) { 7 return 0; 8 } 9 10 int n = matrix[0].length; 11 int[][] dp = new int[2][n]; 12 for (int i = 0; i < n; i++) { 13 dp[0][i] = matrix[0][i] - '0'; 14 result = Math.max(dp[0][i], result); 15 } 16 17 for (int i = 1; i < m; i++) { 18 dp[i % 2][0] = matrix[i][0] - '0'; 19 for (int j = 1; j < n; j++) { 20 if (matrix[i][j] == '1') { 21 dp[i % 2][j] = Math.min(dp[(i - 1) % 2][j],Math.min(dp[(i - 1) % 2][j - 1], dp[i % 2][j - 1])) + 1; 22 result = Math.max(dp[i % 2][j], result); 23 } else { 24 dp[i % 2][j] = 0; 25 } 26 } 27 } 28 29 return result * result; 30 31 } 32 }
2. 序列型動態規划
state: f[i]表示前i個位置/數字/字符, 第i個...
function: f[i] = f[j]... j是i之前的一個位置
initialize: f[0]...
answer: f[n]...
一般answer是f(n)而不是f(n - 1) 因為對於n個最富,包含前0個字符(空串),前1個字符...前n個字符。
注意: 如果不是跟坐標相關的動態規划,一般有n個數/字符,就開n+1個位置的數組。 第0個位置單獨留出來作初始化
2.1 word break

1 public class Solution { 2 public boolean wordBreak(String s, Set<String> wordDict) { 3 boolean canBreak[] = new boolean[s.length() + 1]; 4 canBreak[0] = true; 5 for (int i = 1; i < s.length() + 1; i++) { 6 for (int j = 0; j < i; j++) { 7 if (canBreak[j] == true && wordDict.contains(s.substring(j, i))) { 8 canBreak[i] = true; 9 break; 10 } 11 } 12 } 13 return canBreak[s.length()]; 14 } 15 }
2.2 word break ii
難到爆炸 但是總體上是熟悉的套路!

1 public class Solution { 2 public List<String> wordBreak(String s, Set<String> wordDict) { 3 ArrayList<String> result = new ArrayList<String>(); 4 if (s == null || s.length() == 0) { 5 return result; 6 } 7 8 //坐標型動態規划! o(n^2) 9 // provide o(1) complexity to tell a whether a string is a word 10 boolean[][] isWord = new boolean[s.length()][s.length()]; 11 for (int i = 0; i < s.length(); i++) { 12 for (int j = i; j < s.length(); j++) { 13 if (wordDict.contains(s.substring(i,j + 1))) { 14 isWord[i][j] = true; 15 } 16 } 17 } 18 19 //單序列型動態規划! 20 //檢測從i位置出發 是否能達到末尾位置 21 boolean[] possible = new boolean[s.length() + 1]; 22 possible[s.length()] = true; 23 for (int i = s.length() - 1; i >= 0; i--) { 24 for (int j = i; j < s.length(); j++) { 25 if (isWord[i][j] && possible[j + 1]) { 26 possible[i] = true; 27 break; 28 } 29 } 30 } 31 List<Integer> path = new ArrayList<Integer>(); 32 search(0, s, path, isWord, possible, result); 33 return result; 34 } 35 36 private void search(int index, String s, List<Integer> path, 37 boolean[][] isWord, boolean[] possible, 38 List<String> result) { 39 40 //從index點不能走到末尾 故不需要考慮從index點開始的情況 41 if (!possible[index]) { 42 return; 43 } 44 45 //index 走到末尾了,path里存的斷點坐標轉換為string 46 if (index == s.length()) { 47 StringBuilder sb = new StringBuilder(); 48 int lastIndex = 0; 49 for (int i = 0; i < path.size(); i++) { 50 sb.append(s.substring(lastIndex, path.get(i))); 51 if (i != path.size() - 1) sb.append(" "); 52 lastIndex = path.get(i); 53 } 54 result.add(sb.toString()); 55 return; 56 } 57 58 //遞歸計算從index點出發的路徑 類似於travse a binary tree的方式 59 for (int i = index; i < s.length(); i++) { 60 if (!isWord[index][i]) { 61 continue; 62 } 63 path.add(i + 1); 64 search(i + 1, s, path, isWord, possible, result); 65 path.remove(path.size() - 1); 66 } 67 } 68 }
2.3 House Robber
很簡單的一道單序列動態規划問題 f[i]的狀態只與f[i - 1]和f[i - 2] 相關
state: f[i]
function:max(f[i - 1], f[i - 2] + A[i - 1])
initialize: f[0] = 0; 當沒有house可以搶的時候為0
f[1] = A[0]; 當只有1個house的時候,為house的當前值
answer: f[size]

1 public class Solution { 2 /** 3 * @param A: An array of non-negative integers. 4 * return: The maximum amount of money you can rob tonight 5 */ 6 public long houseRobber(int[] A) { 7 // write your code here 8 if (A == null || A.length == 0) { 9 return 0; 10 } 11 int size = A.length; 12 long[] dp = new long[size + 1]; 13 dp[0] = 0; 14 dp[1] = A[0]; 15 for (int i = 2; i <= size; i++) { 16 dp[i] = Math.max(dp[i - 1], dp[i - 2] + A[i - 1]); 17 } 18 return dp[size]; 19 } 20 }
滾動數組優化:由於f[i]的狀態只與前2個狀態有關,那么其實只用記錄2個狀態量,利用滾動數組把o(n) 優化為o(1)
注意:滾動數組的size取決於 function里面 大問題的答案來自於幾個小問題
function:max(f[i - 1], f[i - 2] + A[i - 1]) 轉化為:
f[i%2] = max(f[(i - 1)%2], (f[(i - 2)%2]+ a[i - 1])) note: 新得到的值覆蓋掉舊數組里的對應的值
觀察我們要保留的狀態來確定模數
house robber version2
state: f[i] 表示前i個房子中,偷到的最大價值
function: f[i%2] = max(f[(i - 1) % 2], (f[(i - 2) % 2] + a[i - 1]));
initialize: f[0] = 0;
f[1] = A[0];

1 public class Solution { 2 public int rob(int[] nums) { 3 //version 2 optimize with 滾動數組 4 if (nums == null || nums.length == 0) { 5 return 0; 6 } 7 int size = nums.length; 8 int[] dp = new int[2]; 9 dp[0] = 0; 10 dp[1] = nums[0]; 11 for (int i = 2; i <= size; i++) { 12 dp[i%2] = Math.max(dp[(i - 1) % 2], dp[(i - 2) % 2] + nums[i - 1]); 13 } 14 return dp[size % 2]; 15 } 16 }
2.4 House Robber ii
After robbing those houses on that street, the thief has found himself a new place for his thievery so that he will not get too much attention. This time, all houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile, the security system for these houses remain the same as for those in the previous street.
Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonightwithout alerting the police.
即首尾點不能同時取
思路,把第一個點放在尾巴處,如果沒有取到尾點,那么再做一次比較,看看是否加入首點。 ❌ 因為是否放入首點,除了考慮是否放入末尾節點外還要考慮是否放入了第二個點。
正確的思路:區分加不加首點,直接2種情況下分別做動態規划!
在不同情況下,分情況做多次dp或者其他算法,不會影響整體的時間復雜度,分情況套用一個helper function!

1 public class Solution { 2 /** 3 * @param nums: An array of non-negative integers. 4 * return: The maximum amount of money you can rob tonight 5 */ 6 public int houseRobber2(int[] nums) { 7 // write your code here 8 if (nums == null || nums.length == 0) { 9 return 0; 10 } 11 int size = nums.length; 12 if (size == 1) { 13 return nums[0]; 14 } 15 return Math.max(helper(nums, 0 , size - 1), helper(nums, 1, size)); 16 } 17 private int helper(int[] nums, int start, int end) { 18 int[] dp = new int[2]; 19 dp[0] = 0; 20 dp[1] = nums[start]; 21 int size = end - start; 22 for (int i = 2; i <= size; i++) { 23 dp[i % 2] = Math.max(dp[(i - 1) % 2], (dp[(i - 2) % 2] + nums[i - 1 + start])); 24 } 25 return dp[size % 2]; 26 } 27 }
2.5 House Robber iii
這道題很重要的在於理解對於當前node的結果 來自於本層當前的child和grandchild的結果。故依賴於前2層的結果,還是需要利用滾動數組做動態規划!
//dp[i][0]表示以i為根的子樹不偷根節點能獲得的最高價值,dp[i][1]表示以i為根的子樹偷根節點能獲得的最高價值
理解這個長度為2的數組是如何在遞歸的時候變化的!

1 /** 2 * Definition for a binary tree node. 3 * public class TreeNode { 4 * int val; 5 * TreeNode left; 6 * TreeNode right; 7 * TreeNode(int x) { val = x; } 8 * } 9 */ 10 public class Solution { 11 public int rob(TreeNode root) { 12 if (root == null) { 13 return 0; 14 } 15 int[] result = helper(root); 16 return Math.max(result[0], result[1]); 17 } 18 19 private int[] helper(TreeNode node) { 20 if (node == null) { 21 return new int[]{0,0}; 22 } 23 24 int[] left = helper(node.left); 25 int[] right = helper(node.right); 26 int[] now = new int[2]; 27 now[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]); 28 now[1] = left[0] + right[0] + node.val; 29 30 return now; 31 } 32 }
2.6 Decode ways
典型的序列型動態規划,記住,開n+1個位置,從空串的位置開始 很重要!!!
空串的初始化,應該為1而不是0,因為空串也作為一個合理的decode方式。
注意,dp[i]的狀態依賴於dp[i - 1] 和 dp[i - 2]
dp[i]是2者的累加和,所以分別考慮是否需要添加該部分

1 public class Solution { 2 public int numDecodings(String s) { 3 if (s == null || s.length() == 0) { 4 return s.length(); 5 } 6 int m = s.length(); 7 int[] dp = new int[m + 1]; 8 dp[0] = 1;//犯錯點: 作為前面的空串也認為是1種解析方式 eg: 空串+10 就是dp[2] = dp[0] = 1 9 dp[1] = s.charAt(0) == '0' ? 0 : 1; 10 for (int i = 2; i <= m; i++) { 11 //分2種情況 12 int temp1 = s.charAt(i - 2) -'0'; 13 int temp2 = s.charAt(i - 1) - '0'; 14 // 1. 1個digits單獨作為一個數 15 if(temp2 > 0) { 16 dp[i] = dp[i - 1]; 17 } 18 19 //2. 2個digits作為1個數 20 int twodigits = temp1 * 10 + temp2; 21 if(twodigits >= 10 && twodigits <= 26) { 22 dp[i] += dp[i - 2]; 23 } 24 } 25 return dp[m]; 26 } 27 }
2.7 Coin Change
dp[i] : 表示組成面值i所需的最小的coin個數。
注意,面值為0 的時候,直接返回0
先把所有的dp[i]都變成max的int
返回的時候,如果dp[k] = Math.MAX_VALUE, 那么返回-1, 這個需要最后單獨做處理

1 public class Solution { 2 public int coinChange(int[] coins, int amount) { 3 if (amount == 0) return 0; 4 int[] dp = new int[amount + 1]; 5 dp[0] = 0; 6 int size = coins.length; 7 for (int i = 1; i <= amount; i++) { 8 dp[i] = Integer.MAX_VALUE; 9 for (int value : coins) { 10 if (i >= value && dp[i - value] != Integer.MAX_VALUE) { 11 dp[i] = Math.min(dp[i - value] + 1, dp[i]); 12 } 13 } 14 } 15 16 17 if(dp[amount] < Integer.MAX_VALUE && dp[amount] > 0) { 18 return dp[amount]; 19 } else { 20 return -1; 21 } 22 23 } 24 }
2.8 Integer break
Given a positive integer n, break it into the sum of at least two positive integers and maximize the product of those integers. Return the maximum product you can get.
For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4).
Note: you may assume that n is not less than 2.
Hint:
- There is a simple O(n) solution to this problem.
- You may check the breaking results of n ranging from 7 to 10 to discover the regularities.
這道題給了我們一個正整數n,讓我們拆分成至少兩個正整數之和,使其乘積最大,題目提示中讓我們用O(n)來解題,而且告訴我們找7到10之間的規律,那么我們一點一點的來分析:
正整數從1開始,但是1不能拆分成兩個正整數之和,所以不能當輸出。
那么2只能拆成1+1,所以乘積也為1。
數字3可以拆分成2+1或1+1+1,顯然第一種拆分方法乘積大為2。
數字4拆成2+2,乘積最大,為4。
數字5拆成3+2,乘積最大,為6。
數字6拆成3+3,乘積最大,為9。
數字7拆為3+4,乘積最大,為12。
數字8拆為3+3+2,乘積最大,為18。
數字9拆為3+3+3,乘積最大,為27。
數字10拆為3+3+4,乘積最大,為36。

1 public class Solution { 2 public int integerBreak(int n) { 3 if (n == 2 || n == 3) { 4 return n - 1; 5 } 6 int res = 1; 7 while (n > 4) { 8 n = n - 3; 9 res = res* 3; 10 } 11 return res*n; 12 } 13 }
我們再來觀察上面列出的10之前數字的規律,我們還可以發現數字7拆分結果是數字4的三倍,而7比4正好大三,數字8拆分結果是數字5的三倍,而8比5大3,后面都是這樣的規律,那么我們可以把數字6之前的拆分結果都列舉出來,然后之后的數通過查表都能計算出來,參見代碼如下;

1 public class Solution { 2 public int integerBreak(int n) { 3 int[] dp = new int[]{0, 0, 1, 2, 4, 6, 9}; 4 5 6 int res = 1; 7 while (n > 6) { 8 n = n - 3; 9 res = res* 3; 10 } 11 return res * dp[n]; 12 } 13 }
3. 雙序列動態規划
state: f[i][j]代表了第一個sequence的前i個數字/字符,配上第二個sequence的前j個...
function: f[i][j] = 研究第i個和第j個的匹配關系
initialize: f[i][0] 和f[0][i]
answer: f[n][m]
n = s1.length(); m = s2.length()
3.1 Longest Common Subsequence
LCS經典問題
state:f[i][j] 表示第一個字符串的前i個字符配上第二個字符串的前j個字符的LCS長度
function:注意研究的是第i個字符和第j個字符的關系 a[i - 1] 與 b[j - 1]的關系
if (a[i - 1] == b[j - 1]) {
f[i][j] = f[i - 1][j - 1] + 1;
} else {
f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
}
initialize:f[i][0] = 0; f[0][j] = 0;
answer:f[n][m];

1 public class Solution { 2 /** 3 * @param A, B: Two strings. 4 * @return: The length of longest common subsequence of A and B. 5 */ 6 public int longestCommonSubsequence(String A, String B) { 7 // write your code here 8 if (A == null || B == null) { 9 return 0; 10 } 11 12 int m = A.length(); 13 int n = B.length(); 14 if (m == 0 || n == 0) { 15 return 0; 16 } 17 int[][] lcs = new int[m + 1][n + 1]; 18 for (int i = 0; i <= m; i++) { 19 lcs[i][0] = 0; 20 } 21 for (int i = 0; i <= n; i++) { 22 lcs[0][i] = 0; 23 } 24 25 for (int i = 1; i <=m; i++) { 26 for (int j = 1; j <= n; j++) { 27 if (A.charAt(i - 1) == B.charAt(j - 1)) { 28 lcs[i][j] = lcs[i - 1][j - 1] + 1; 29 } else { 30 lcs[i][j] = Math.max(lcs[i - 1][j], lcs[i][j - 1]); 31 } 32 } 33 } 34 return lcs[m][n]; 35 } 36 }
類似問題: 比較當前的char是否相同 Longest Common Substring
很巧妙的在while循環里利用 i+len 作為控制條件!

1 public class Solution { 2 /** 3 * @param A, B: Two string. 4 * @return: the length of the longest common substring. 5 */ 6 public int longestCommonSubstring(String A, String B) { 7 // write your code here 8 if (A == null || B == null) { 9 return 0; 10 } 11 12 int maxLen = 0; 13 int m = A.length(); 14 int n = B.length(); 15 if (m == 0 || n == 0) { 16 return 0; 17 } 18 19 for (int i = 0; i < m; i++) { 20 for (int j = 0; j < n; j++) { 21 int len = 0; 22 while (i + len < m && j + len < n && 23 A.charAt(i + len) == B.charAt(j + len)){ 24 len++; 25 if(len > maxLen) 26 maxLen = len; 27 } 28 } 29 } 30 31 return maxLen; 32 } 33 }
3.2 Edit distance
state: f[i][j] 表示a的前i個字符最少葯幾次編輯可以變成b的前j個字符
function: if (a[i - 1] == b [j - 1]) {
dis[i][j] = dis[i - 1][j - 1];
} else {
dis[i][j] = Math.min(dis[i - 1][j] + dis[i][j - 1]) + 1; // 注意 3種情況 不是2種 ❌
dis[i][j] = Math.min(dis[i - 1][j], Math.min(dis[i][j - 1], dis[i - 1][j - 1])) + 1; //✅
}
initialize: f[i][0] = i; f[0][j] = j;
answer: f[m][n]

1 public class Solution { 2 public int minDistance(String word1, String word2) { 3 if (word1 == null || word2 == null) { 4 return 0; 5 } 6 int m = word1.length(); 7 int n = word2.length(); 8 if (m == 0 || n == 0) { 9 return Math.max(m, n); 10 } 11 12 int[][] dis = new int[m + 1][n + 1]; 13 for (int i = 0; i <= m; i++) { 14 dis[i][0] = i; 15 } 16 for (int i = 0; i <= n; i++) { 17 dis[0][i] = i; 18 } 19 20 for (int i = 1; i <= m; i++) { 21 for (int j = 1; j <= n; j++) { 22 if (word1.charAt(i - 1) == word2.charAt(j - 1)) { 23 dis[i][j] = dis[i - 1][j - 1]; 24 } else { 25 dis[i][j] = Math.min(dis[i - 1][j], Math.min(dis[i][j - 1], dis[i - 1][j - 1])) + 1; 26 } 27 } 28 } 29 return dis[m][n]; 30 } 31 }
3.3 Edit distance
state: f[i][j] 表示a的前i個字符和b的前j個字符能否交替組成s3的前i + j個字符
function: if (f[i - 1][j] == true && a.charAt(i - 1) == s3.charAt(i + j - 1)
|| f[i][j - 1] == true && b.charAt(j - 1)) {
f[i][j] = true;
} else {
f[i][j] = false;
}
initialize: f[i][0] = i; f[0][i] = i;
answer: f[m][n]

3.4 Distinct Subsequence
state: f[i][j]表示s的前i個字符中選取t的前j個字符 有多少種方案
function:
if (a[i - 1] == b [j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + dp[i][j - 1];
} else {
dis[i][j] =dp[i - 1][j];
}
initialize: f[i][0] = 1; 當目標為空串時,無論source的長度是多少都認為是1個
f[0][j] = 0; (j > 0) 當兩個都為空串的時候,認為是1
answer: f[m][n]

1 public class Solution { 2 public int numDistinct(String s, String t) { 3 int m = s.length(); 4 int n = t.length(); 5 6 int[][] dp = new int[m + 1][n + 1]; 7 for (int i = 0; i <= m; i++) { 8 dp[i][0] = 1; 9 } 10 for (int i = 1; i <=n; i++){ 11 dp[0][i] = 0; 12 } 13 14 15 for (int i = 1; i <= m; i++) { 16 for (int j = 1; j <=n; j++) { 17 if (s.charAt(i - 1) == t.charAt(j - 1)) { //經常忘了index要減1 18 dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 19 } else { 20 dp[i][j] = dp[i - 1][j]; 21 } 22 } 23 } 24 return dp[m][n]; 25 } 26 }
3.5 Distinct Subsequence
state: f[i][j]表示s的前i個字符中選取t的前j個字符 有多少種方案
function:
if (a[i - 1] == b [j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + dp[i][j - 1];
} else {
dis[i][j] =dp[i - 1][j];
}
initialize: f[i][0] = 1; 當目標為空串時,無論source的長度是多少都認為是1個
f[0][j] = 0; (j > 0) 當兩個都為空串的時候,認為是1
answer: f[m][n]

1 public class Solution { 2 public boolean isInterleave(String s1, String s2, String s3) { 3 4 if (s1.length() + s2.length() != s3.length()) { 5 return false; 6 } 7 8 int m = s1.length(); 9 int n = s2.length(); 10 11 12 boolean[][] dp = new boolean[m + 1][n + 1]; 13 14 dp[0][0] = true; 15 for (int i = 1; i <= m; i++) { 16 if(s3.charAt(i - 1) == s1.charAt(i - 1) && dp[i - 1][0]) { 17 dp[i][0] = true; 18 } 19 } 20 for (int j = 1; j <= n; j++) { 21 if (dp[0][j - 1] == true && s2.charAt(j - 1) == s3.charAt(j - 1)) { 22 dp[0][j] = true; 23 } 24 } 25 26 for (int i = 1; i <= m; i++) { 27 for (int j = 1; j <= n; j++) { 28 // if (dp[i - 1][j - 1] == true && s3.charAt(i + j - 1) == s1.charAt(i - 1) || s3.charAt(i + j -1) == s2.charAt(j - 1)) { 錯誤的寫法1 29 // if ((dp[i - 1][j - 1] && s3.charAt(i + j - 1) == s1.charAt(i - 1)) 30 // || (dp[i - 1][j - 1] && s3.charAt(i + j - 1) == s2.charAt(j - 1))) {//錯誤的寫法2 31 if((dp[i - 1][j] && s3.charAt(i + j - 1) == s1.charAt(i - 1)) 32 || (dp[i][j - 1] && s3.charAt(i + j - 1) == s2.charAt(j - 1))) { 33 dp[i][j] = true; 34 } 35 36 } 37 } 38 return dp[m][n]; 39 } 40 }
4. 划分型動態規划
在一個大的區間內找一個小的區間
划分類的題目,基本思路都是用一個local數組和一個gobal數組,然后進行遍歷。
之所以可以用變量來代替數組,是因為做了滾動數組的優化!
4.1Maximum Subarray
在一個數組里找一個連續的部分, 使得累加和最大
state: local[i] 表示包括第i個元素能找到的最大值
gobal[i] 表示全局前i個元素中能找到的最大值
function:
local[i] = max(local[i - 1] + nums[i], nums[i]);
gobal[i] = max(gobal[i - 1], local[i]);
initialization:
local[0] = gobal[0] = nums[0];
answer: gobal[size - 1];
代碼:
這個代碼會超時!

1 public class Solution { 2 public int maxSubArray(int[] nums) { 3 int size = nums.length; 4 int[] local = new int[size]; 5 int[] gobal = new int[size]; 6 local[0] = nums[0]; 7 gobal[0] = nums[0]; 8 for (int i = 1; i < size; i++) { 9 local[i] = Math.max(nums[i], local[i - 1] + nums[i]); 10 gobal[i] = Math.max(local[i], gobal[i - 1]); 11 } 12 return gobal[size - 1]; 13 14 } 15 }
優化:從以上代碼,能找到一個精髓的思路在於 local的變量,在拿前i個的最大值+ nums[i]和第i個值比較. 也就是說,當local[i - 1] < 0 的時候,就丟掉前面的部分!從而,local[i - 1]可以轉化為一個prefix sum的概念!

1 public class Solution { 2 public int maxSubArray(int[] nums) { 3 if (nums == null || nums.length == 0) { 4 return 0; 5 } 6 int size = nums.length; 7 int preSum = nums[0]; 8 int max = preSum; 9 10 for (int i = 1; i < size; i++) { 11 preSum = Math.max(preSum + nums[i], nums[i]); 12 max = Math.max(preSum, max); 13 } 14 return max; 15 } 16 }
更簡潔的判斷presum是否大於0的寫法

1 public class Solution { 2 public int maxSubArray(int[] nums) { 3 if (nums == null || nums.length == 0) { 4 return 0; 5 } 6 int size = nums.length; 7 int preSum = nums[0]; 8 int max = preSum; 9 10 for (int i = 1; i < size; i++) { 11 if (preSum > 0) { 12 preSum += nums[i]; 13 } 14 else { 15 preSum = nums[i]; 16 } 17 max = Math.max(max, preSum); 18 } 19 return max; 20 } 21 }
另外一種寫法,記錄從開始到當前的一個最小值,用當前的累加和剪掉最小值就得到了當前最大的subarray sum
eg:掃描數組到紅色1的位置是,minSum 去除掉了藍色部分即得到了中間的1的部分
- 1, - 1, 1, 1 , 1 , 1, -1

1 public class Solution { 2 /** 3 * @param nums: A list of integers 4 * @return: A integer indicate the sum of max subarray 5 */ 6 public int maxSubArray(int[] A) { 7 if (A == null || A.length == 0) { 8 return 0; 9 } 10 11 int sum = 0, max = Integer.MIN_VALUE, minSum = 0; 12 for (int i = 0; i < A.length; i++) { 13 sum += A[i]; 14 max = Math.max(max, sum - minSum); 15 minSum = Math.min(minSum, sum); 16 } 17 return max; 18 } 19 }
其實presum的寫法是用1次greedy,找到當前累加最大值,把小於0的前綴扔掉。
minSum寫法的話用了2次greedy,找到當前累加的最大值和累加最小值,用最大值剪掉最小值,由於加法的特性,可以直接扔掉不找到最小值也能拿到答案!

public class Solution { /** 馬甲變形題,千萬注意區分local和gobal的區別,local指的是包含nums[i]的這個最值 * @param nums: A list of integers * @return: An integer indicate the value of maximum difference between two * Subarrays */ public int maxDiffSubArrays(int[] nums) { // write your code here int size = nums.length; int[] left_max = new int[size]; int[] left_min = new int[size]; int[] right_max = new int[size]; int[] right_min = new int[size]; int localMax = nums[0]; int localMin = nums[0]; left_max[0] = left_min[0] = nums[0]; //search for left_max for (int i = 1; i < size; i++) { localMax = Math.max(nums[i], localMax + nums[i]); left_max[i] = Math.max(left_max[i - 1], localMax); } //search for left_min for (int i = 1; i < size; i++) { localMin = Math.min(nums[i], localMin + nums[i]); left_min[i] = Math.min(left_min[i - 1], localMin); } right_max[size - 1] = right_min[size - 1] = nums[size - 1]; //search for right_max localMax = nums[size - 1]; for (int i = size - 2; i >= 0; i--) { localMax = Math.max(nums[i], localMax + nums[i]); right_max[i] = Math.max(right_max[i + 1], localMax); } //search for right min localMin = nums[size - 1]; for (int i = size - 2; i >= 0; i--) { localMin = Math.min(nums[i], localMin + nums[i]); right_min[i] = Math.min(right_min[i + 1], localMin); } //search for separete position int diff = 0; for (int i = 0; i < size - 1; i++) { diff = Math.max(Math.abs(left_max[i] - right_min[i + 1]), diff); diff = Math.max(Math.abs(left_min[i] - right_max[i + 1]), diff); } return diff; } }
4.2 Maximum Product Subarray (Maximum Subarray的馬甲變形,總體思路還是一樣的)
這道題跟MaximumSubarray模型上和思路上都比較類似,還是用一維動態規划中的“局部最優和全局最優法”。這里的區別是維護一個局部最優不足以求得后面的全局最優,這是由於乘法的性質不像加法那樣,累加結果只要是正的一定是遞增,乘法中有可能現在看起來小的一個負數,后面跟另一個負數相乘就會得到最大的乘積。不過事實上也沒有麻煩很多,我們只需要在維護一個局部最大的同時,在維護一個局部最小,這樣如果下一個元素遇到負數時,就有可能與這個最小相乘得到當前最大的乘積和,這也是利用乘法的性質得到的。代碼如下:

1 public class Solution { 2 public int maxProduct(int[] nums) { 3 if (nums == null || nums.length == 0) { 4 return 0; 5 } 6 7 int size = nums.length; 8 int[] min = new int[size]; 9 int[] max = new int[size]; 10 min[0] = nums[0]; 11 max[0] = nums[0]; 12 int result = max[0]; 13 14 for (int i = 1; i < size; i++) { 15 if (nums[i] > 0) { 16 max[i] = Math.max(nums[i], max[i - 1] * nums[i]); 17 min[i] = Math.min(nums[i], min[i - 1] * nums[i]); 18 } else { 19 max[i] = Math.max(nums[i], min[i - 1] * nums[i]); 20 min[i] = Math.min(nums[i], max[i - 1] * nums[i]); 21 } 22 result = Math.max(result, max[i]); 23 24 } 25 return result; 26 } 27 }
利用滾動數組,把max和min數組優化為o(1)的變量

1 public class Solution { 2 public int maxProduct(int[] A) { 3 if(A==null || A.length<1) return 0; 4 if(A.length < 2) return A[0]; 5 6 int global = A[0]; 7 int max = A[0], min = A[0]; 8 for(int i=1; i<A.length; i++) { 9 int a = max*A[i]; 10 int b = min*A[i]; 11 12 max = Math.max(A[i], Math.max(a, b)); 13 min = Math.min(A[i], Math.min(a, b)); 14 global = Math.max(max, global); 15 } 16 17 return global; 18 } 19 }
相關問題: 股票問題
股票1

1 public class Solution { 2 public int maxProfit(int[] prices) { 3 if (prices == null || prices.length == 0) { 4 return 0; 5 } 6 int m = prices.length; 7 int max = 0; 8 int min = prices[0]; 9 for (int i = 1; i < m; i++) { 10 min = Math.min(min, prices[i]); 11 max = Math.max(max, prices[i] - min); 12 } 13 return max; 14 } 15 }
股票2

1 public class Solution { 2 public int maxProfit(int[] prices) { 3 int profit = 0; 4 for (int i = 0; i < prices.length - 1; i++) { 5 int diff = prices[i+1] - prices[i]; 6 if (diff > 0) { 7 profit += diff; 8 } 9 } 10 return profit; 11 } 12 }
股票3
對於2次的題目 一定要想到從左到右和從右到左2次!!!

1 public class Solution { 2 public int maxProfit(int[] prices) { 3 if (prices == null || prices.length <= 1) { 4 return 0; 5 } 6 7 int[] left = new int[prices.length]; 8 int[] right = new int[prices.length]; 9 10 // DP from left to right; 11 left[0] = 0; 12 int min = prices[0]; 13 for (int i = 1; i < prices.length; i++) { 14 min = Math.min(prices[i], min); 15 left[i] = Math.max(left[i - 1], prices[i] - min); 16 } 17 18 //DP from right to left; 19 right[prices.length - 1] = 0; 20 int max = prices[prices.length - 1]; 21 for (int i = prices.length - 2; i >= 0; i--) { 22 max = Math.max(prices[i], max); 23 right[i] = Math.max(right[i + 1], max - prices[i]); 24 } 25 26 int profit = 0; 27 for (int i = 0; i < prices.length; i++){ 28 profit = Math.max(left[i] + right[i], profit); 29 } 30 31 return profit; 32 } 33 }
股票4
當第i天的價格高於第i-1天(即diff > 0)時,那么可以把這次交易(第i-1天買入第i天賣出)跟第i-1天的交易(賣出)合並為一次交易,即local[i][j]=local[i-1][j]+diff;
當第i天的價格不高於第i-1天(即diff<=0)時,那么local[i][j]=global[i-1][j-1]+diff,而由於diff<=0,所以可寫成local[i][j]=global[i-1][j-1]。
global[i][j]就是我們所求的前i天最多進行k次交易的最大收益,可分為兩種情況:如果第i天沒有交易(賣出),那么global[i][j]=global[i-1][j];如果第i天有交易(賣出),那么global[i][j]=local[i][j]。

1 public class Solution { 2 public int maxProfit(int k, int[] prices) { 3 if (k == 0) { 4 return 0; 5 } 6 int m = prices.length; 7 if (k >= m / 2) { 8 int profit = 0; 9 for (int i = 1; i < m; i++) { 10 if (prices[i] > prices[i - 1]) { 11 profit += (prices[i] - prices[i - 1]); 12 } 13 } 14 return profit; 15 } 16 17 int[][] mustsell = new int[m + 1][m + 1];// mustSell[i][j] 表示前i天,至多進行j次交易,第i天必須sell的最大獲益 18 int[][] gobalmax = new int[m + 1][m + 1];// globalbest[i][j] 表示前i天,至多進行j次交易,第i天可以不sell的最大獲益 19 20 mustsell[0][0] = gobalmax[0][0] = 0; 21 //day zero profit equals 0 22 for (int i = 1; i <= k; i++) { 23 mustsell[0][i] = gobalmax[0][i] = 0; 24 } 25 26 for (int i = 1; i < m; i++) { 27 int gainorlose = prices[i] - prices[i - 1]; 28 mustsell[i][0] = 0; 29 for (int j = 1; j <= k; j++) { 30 //第一部分為第i天價格高於第i - 1 天,那么可以把第i-1天的交易合並到當天來 31 //第二部分為第i天的價格低於第i - 1 天, 那么就是用gobal的i - 1天的 j - 1次交易來減掉這次虧損的 32 mustsell[i][j] = Math.max(mustsell[i - 1][j] + gainorlose, gobalmax[i - 1][j - 1] + gainorlose); 33 gobalmax[i][j] = Math.max(gobalmax[(i - 1)][j], mustsell[i][j]); 34 35 } 36 } 37 return gobalmax[(m - 1)][k]; 38 39 } 40 }
對於k次交易,k次切分的題目,要想到用動態規划去遞增這個交易的次數,成為動歸的一個維度。
對於k = 2 的特殊情況,要想到從左邊,右邊分別做2次動歸就行了!
4.3 Maximum Subarray II
需要找2段subarray的和
思路: 對於兩次划分的問題,1. 從左到右 2. 從右到左 做2次單次划分的問題,最后用一次for loop 遍歷,尋找2次的切分點
對於最后尋找切分點,需要使用gobal的數組!!!千萬注意

1 public class Solution { 2 /** 3 * @param nums: A list of integers 4 * @return: An integer denotes the sum of max two non-overlapping subarrays 5 */ 6 public int maxTwoSubArrays(ArrayList<Integer> nums) { 7 // write your code 8 if (nums == null || nums.size() == 0) { 9 return 0; 10 } 11 int size = nums.size(); 12 int max = Integer.MIN_VALUE; 13 14 //left to right 15 int leftLocal[] = new int[size]; 16 int leftGobal[] = new int[size]; 17 leftGobal[0] = leftLocal[0] = nums.get(0); 18 for (int i = 1; i < size; i++) { 19 leftLocal[i] = Math.max(leftLocal[i - 1] + nums.get(i), nums.get(i)); 20 leftGobal[i] = Math.max(leftLocal[i], leftGobal[i - 1]); 21 } 22 23 //right to left 24 int rightLocal[] = new int[size]; 25 int rightGobal[] = new int[size]; 26 rightLocal[size - 1] = nums.get(size - 1); 27 rightGobal[size - 1] = nums.get(size - 1); 28 29 for (int i = size - 2; i > 0; i--) { 30 rightLocal[i] = Math.max(rightLocal[i + 1] + nums.get(i), nums.get(i)); 31 rightGobal[i] = Math.max(rightLocal[i], rightGobal[i + 1]); 32 } 33 34 //get total 35 for (int i = 1; i < size; i++) { 36 37 max = Math.max(leftGobal[i - 1] + rightGobal[i], max); 38 } 39 40 return max; 41 } 42 }
對於local數組,可以使用滾動數組進行優化為變量,只保留gobal數組。
對於local數組進行滾動數組優化的代碼如下:

1 public class Solution { 2 /** 3 * @param nums: A list of integers 4 * @return: An integer denotes the sum of max two non-overlapping subarrays 5 */ 6 public int maxTwoSubArrays(ArrayList<Integer> nums) { 7 // write your code 8 int size = nums.size(); 9 int[] left = new int[size]; 10 int[] right = new int[size]; 11 int sum = 0; 12 int minSum = 0; 13 int max = Integer.MIN_VALUE; 14 for(int i = 0; i < size; i++){ 15 sum += nums.get(i); 16 max = Math.max(max, sum - minSum); 17 minSum = Math.min(sum, minSum); 18 left[i] = max; 19 } 20 sum = 0; 21 minSum = 0; 22 max = Integer.MIN_VALUE; 23 for(int i = size - 1; i >= 0; i--){ 24 sum += nums.get(i); 25 max = Math.max(max, sum - minSum); 26 minSum = Math.min(sum, minSum); 27 right[i] = max; 28 } 29 max = Integer.MIN_VALUE; 30 for(int i = 0; i < size - 1; i++){ 31 max = Math.max(max, left[i] + right[i + 1]); 32 } 33 return max; 34 } 35 }
4.4 Maximum Subarray III
對於N次切分,除了使用local和gobal數組,還需要多開1個維度的變量記錄切分次數!
State:
local[i][j]: 表示前i 個數包含第i個元素進行j次操作的最大值
global[i][j]: 表示前i個數進行j次操作的最大值
function:
local[i][j] = max(local[i - 1][j] + nums[i],
global[i - 1][j - 1] + nums[i]);
gobal[i][j] = max(global[i - 1][j],
local[i][j])
initialization:
local[i][0] = global[i][0] = 0;
Answer:
global[len][k]

1 public class Solution { 2 /** 3 * @param nums: A list of integers 4 * @param k: An integer denote to find k non-overlapping subarrays 5 * @return: An integer denote the sum of max k non-overlapping subarrays 6 */ 7 public int maxSubArray(int[] nums, int k) { 8 // write your code here 9 if (nums.length < k) { 10 return 0; 11 } 12 int length = nums.length; 13 14 int[][] localMax = new int[k + 1][length + 1]; 15 int[][] globalMax = new int[k + 1][length + 1]; 16 17 for (int i = 0; i <= k; i++) { 18 localMax[i][0] = 0; 19 globalMax[i][0] = 0; 20 } 21 22 for (int i = 1; i <= k; i++) { 23 localMax[i][i-1] = Integer.MIN_VALUE; 24 //小於 i 的數組不能夠partition 25 for (int j = i; j <= length; j++) { 26 localMax[i][j] = Math.max(localMax[i][j - 1], globalMax[i - 1][j - 1]) + nums[j-1]; 27 28 /* 1. localMax[i][j - 1] + nums[j - 1] 29 相當於把最后一次划分向后移一格 30 2. globalMax[i - 1][j-1]) + nums[j-1] 31 相當於最后加的一個數作為一個獨立的划分! 32 */ 33 34 if (j == i)//千萬注意 i == j的時候 只能一種划分! 35 globalMax[i][j] = localMax[i][j]; 36 else 37 globalMax[i][j] = Math.max(globalMax[i][j-1], localMax[i][j]); 38 39 } 40 } 41 return globalMax[k][length]; 42 } 43 }
best time to buy and sell stock
5. 背包型動態規划
特點:
1. 用值作為dp維度
2. dp過程就是填寫矩陣
3. 可以用滾動數組進行優化
5.1 BackPack
Given n items with size Ai, an integer m denotes the size of a backpack. How full you can fill this backpack?
2. f[i - 1][j] //放不下當前第i 個物品,那么它的結果和i - 1個物品是一致的
注意分析這兩種情況,並不是並列的,大多數情況下都是和i - 1 個物品是一致的,只有當“破例”的時候,也就是說能裝下第i個物品,同時i - 1個物品在j - a[i]容量時也為true. 注意在這部分代碼上的處理,很巧妙!

1 f[0][0] = true; 2 for (int i = 0; i < A.length; i++) { 3 for (int j = 0; j <= m; j++) { 4 f[i + 1][j] = f[i][j]; 5 if (j >= A[i] && f[i][j - A[i]]) { 6 f[i + 1][j] = true; 7 } 8 } // for j 9 } // for i
answer: 檢查f[n][j] 碰到的第一個為真的即為最大值
注意:代碼上 i的下標的處理,需要注意,通常為dp[i][j] = dp[i - 1][..] 但是按答案這樣處理比較簡潔。完整代碼如下

1 public class Solution { 2 /** 3 * @param m: An integer m denotes the size of a backpack 4 * @param A: Given n items with size A[i] 5 * @return: The maximum size 6 */ 7 public int backPack(int m, int[] A) { 8 int n = A.length; 9 boolean dp[][] = new boolean[n + 1][m + 1]; 10 11 dp[0][0] = true; 12 for (int i = 0; i < n; i++) { 13 for (int j = 0; j <= m; j++) { 14 dp[i + 1][j] = dp[i][j]; 15 if (j >= A[i] && dp[i][j - A[i]]) { 16 dp[i + 1][j] = true; 17 } 18 } 19 } 20 21 for (int i = m; i >= 0; i--) { 22 if (dp[n][i]) { 23 return i; 24 } 25 } 26 return 0; 27 } 28 }
5.2 backpack ii
f[i][j]表示前i個物品當中選去一些物品組成容量為j的最大價值
下面是沒有做滾動數組優化的代碼:

1 public class Solution { 2 /** 3 * @param m: An integer m denotes the size of a backpack 4 * @param A & V: Given n items with size A[i] and value V[i] 5 * @return: The maximum value 6 */ 7 public int backPackII(int m, int[] A, int V[]) { 8 // write your code here 9 int n = A.length; 10 int[][] dp = new int[n + 1][m + 1]; 11 12 dp[0][0] = 0; 13 for (int i = 0; i < n; i++) { 14 for (int j = 1; j <= m; j++) { 15 dp[i + 1][j] = dp[i][j]; 16 if (j >= A[i]) { 17 dp[i + 1][j] = Math.max(dp[i][j], dp[i][j - A[i]] + V[i]); 18 } 19 } 20 } 21 return dp[n][m]; 22 } 23 }
5.3 k sum
從n個數中 取k個數,組成和為target
state: f[i][j][t]前i 個數中去j個數出來能否組成和為t
function: f[i][j][t] = f[i - 1][j][t] + f[i - 1][j - 1][t - a[i - 1]]
不包括第i 個數,組成t的情況+ 包括第i個數組成t的情況
犯過的錯誤:1. 循環的循序問題!
2.初始化千萬注意,對於target = 0, k = 0的時候 是有1種取法,即啥都不取!!!

1 public class Solution { 2 /** 3 * @param A: an integer array. 4 * @param k: a positive integer (k <= length(A)) 5 * @param target: a integer 6 * @return an integer 7 */ 8 public int kSum(int A[], int k, int target) { 9 int m = A.length; 10 int dp[][][] = new int[m + 1][k + 1][target + 1]; 11 // i indicates the item index, j indicate capacity, t means max k 12 dp[0][0][0] = 0; 13 14 //note!! 15 for (int i = 0; i <= m; i++) { 16 dp[i][0][0] = 1; 17 } 18 19 for (int i = 0; i < m; i++) { 20 for (int j = 1; j <= k && j <= i + 1; j++) { 21 for (int t = 1; t <= target; t++) { 22 dp[i + 1][j][t] = 0; 23 if (A[i] <= t) { 24 dp[i + 1][j][t] = dp[i][j - 1][t - A[i]]; 25 } 26 dp[i + 1][j][t] += dp[i][j][t]; 27 } 28 } 29 } 30 return dp[m][k][target]; 31 } 32 }
7. 記憶化搜索
我們常見的動態規划問題,比如流水線調度問題,矩陣鏈乘問題等等都是“一步接着一步解決的”,即規模為 i 的問題需要基於規模 i-1 的問題進行最優解選擇,通常的遞歸模式為DP(i)=optimal{DP(i-1)}。而記憶化搜索本質上也是DP思想,當子問題A和子問題B存在子子問題C時,如果子子問題C的最優解已經被求出,那么子問題A或者是B只需要“查表”獲得C的解,而不需要再算一遍C。記憶化搜索的DP模式比普通模式要“隨意一些”,通常為DP(i)=optimal(DP(j)), j < i。
7.1.1 Longest Increasing continuous subsequence
Give an integer array,find the longest increasing continuous subsequence in this array.
An increasing continuous subsequence:
- Can be from right to left or from left to right.
- Indices of the integers in the subsequence should be continuous.

1 public class Solution { 2 /** 3 * @param A an array of Integer 4 * @return an integer 5 */ 6 public int longestIncreasingContinuousSubsequence(int[] A) { 7 // Write your code here 8 if (A == null || A.length == 0) { 9 return 0; 10 } 11 int len = 1; 12 int res = 1; 13 for (int i = 1; i < A.length; i++) { 14 if (A[i] > A[i - 1]) { 15 len++; 16 res = Math.max(res,len); 17 } else { 18 len = 1; 19 } 20 } 21 22 len = 1; 23 for (int i = A.length - 2; i >= 0; i--) { 24 if (A[i + 1] < A[i]) { 25 len++; 26 res = Math.max(len, res); 27 } else { 28 len = 1; 29 } 30 } 31 return res; 32 } 33 }
7.1.1 Longest Increasing continuous subsequence 2D
Give you an integer matrix (with row size n, column size m),find the longest increasing continuous subsequence in this matrix. (The definition of the longest increasing continuous subsequence here can start at any row or column and go up/down/right/left any direction).
Given a matrix:
[ [1 ,2 ,3 ,4 ,5], [16,17,24,23,6], [15,18,25,22,7], [14,19,20,21,8], [13,12,11,10,9] ]
return 25
對於這道題,用傳統的多重循環遇到困難:
- 從上到下的循環不能解決問題
- 初始狀態找不到
暴力的方法,從每個點深度優先搜索。
記憶化搜索vs 普通搜索
區別在與用flag數組和dp數組來記錄我們曾經遍歷過的值,以及這個值的最優解
flag數組的作用是保證每個點只遍歷一次!!!

flag[i][j]表示 i, j 這個點是否遍歷過, 如果遍歷過那么直接返回dp[i][j]里面保存的結果就好了!
state:dp[x][y] 以x,y作為結尾的最長子序列
function:
遍歷x,y上下左右4個格子
dp[x][y] = dp[nx][ny] + 1 (if a[x][y] > a[nx][ny]);
intialize:
dp[x][y]是極小值時,初始化為1 //表示以xy作為結尾的最長子序列至少是有1個!
answer: dp[x][y]中的最大值
這種做法保證了以每個點作為最長子序列的結尾的情況,只會遍歷一次。對於已經遍歷過的點dp[x][y] 一定是最優解!
1 public class Solution { 2 /** 3 * @param A an integer matrix 4 * @return an integer 5 */ 6 public int longestIncreasingContinuousSubsequenceII(int[][] A) { 7 // Write your code here 8 if(A.length == 0) { 9 return 0; 10 } 11 int m = A.length; 12 int n = A[0].length; 13 boolean[][] flag = new boolean[m][n]; 14 int[][] dp = new int[m][n]; 15 int res = 0; 16 17 for (int i = 0; i < m; i++) { 18 for (int j = 0; j < n; j++) { 19 dp[i][j] = 1;//長度至少為1,做初始化 20 } 21 } 22 23 for (int i = 0; i < m; i++) { 24 for (int j = 0; j < n; j++) { 25 dp[i][j] = search(i, j, dp, flag, A); 26 res = Math.max(res,dp[i][j]); 27 } 28 } 29 return res; 30 } 31 32 int[] array1 = {0, 1, 0, -1, 0}; 33 private int search(int x, int y, int[][] dp, boolean[][] flag, int[][] A) { 34 if (flag[x][y] == true) { 35 return dp[x][y]; 36 } 37 38 for (int k = 0; k < 4; k++) { 39 int newx = x + array1[k]; 40 int newy = y + array1[k + 1]; 41 if (newx < A.length && newx >= 0 && newy >= 0 && newy < A[0].length) { 42 if (A[x][y] > A[newx][newy]) { 43 dp[x][y] = Math.max(search(newx, newy, dp, flag, A) + 1, dp[x][y]); 44 } 45 } 46 47 } 48 flag[x][y] = true; 49 return dp[x][y]; 50 } 51 } 52
注意,九章版本的初始化是在search的同時做的,第40行,ans = 1然后max(ans,search) 很巧妙!

1 public class Solution { 2 /** 3 * @param A an integer matrix 4 * @return an integer 5 */ 6 int [][]dp; 7 int [][]flag ; 8 int n ,m; 9 public int longestIncreasingContinuousSubsequenceII(int[][] A) { 10 if(A.length == 0) 11 return 0; 12 m = A.length; 13 n = A[0].length; 14 int ans= 0; 15 dp = new int[m][n]; 16 flag = new int[m][n]; 17 18 for(int i = 0; i < m; i++) { 19 for(int j = 0; j < n; j++) { 20 dp[i][j] = search(i, j, A); 21 ans = Math.max(ans, dp[i][j]); 22 } 23 } 24 return ans; 25 } 26 int []dx = {1,-1,0,0}; 27 int []dy = {0,0,1,-1}; 28 29 int search(int x, int y, int[][] A) { 30 if(flag[x][y] != 0) 31 return dp[x][y]; 32 33 int ans = 1; 34 int nx , ny; 35 for(int i = 0; i < 4; i++) { 36 nx = x + dx[i]; 37 ny = y + dy[i]; 38 if(0<= nx && nx < m && 0<= ny && ny < n ) { 39 if( A[x][y] > A[nx][ny]) { 40 ans = Math.max(ans, search( nx, ny, A) + 1); 41 } 42 } 43 } 44 flag[x][y] = 1; 45 dp[x][y] = ans; 46 return ans; 47 } 48 }
7.2 記憶化搜索與博弈類動態規划結合
對於博弈類的問題dp[i]只定義一個人的狀態,不要同時定義2個人的狀態!千萬注意!!!
dp數組,只記錄一個人的狀態,但是更新的時候要考慮2個人的狀態做更新,也就是說第二個人的決策是使得第一個人的結果盡量小!!!
7.2.1 Coins in a Line
state: dp[i] 表示現在還剩下i個硬幣,前者最后輸贏狀況
function: dp[n] = (!dp[n - 1]) || (!dp[n - 2])
//對於這道題而言,只要前面2個狀態不全為true,那么當前的狀態就true 其實可以從前往后進行動態規划
//從前往后動態規划太簡單,下面還是用記憶化搜索的方式,從大問題,遞歸到小問題!
intialize: dp[0] = false;
dp[1] = true;
dp[2] = true;
answer: dp[n]
用遞歸的方式把大問題轉化為小問題
畫搜索樹,理解大問題如何轉化為小問題!

1 public class Solution { 2 /** 3 * @param n: an integer 4 * @return: a boolean which equals to true if the first player will win 5 */ 6 public boolean firstWillWin(int n) { 7 // write your code here 8 if (n < 3) { 9 return n > 0; 10 } 11 boolean[] dp = new boolean[n + 1]; 12 boolean[] flag = new boolean[n + 1]; 13 dp[n] = search(n, dp, flag); 14 return dp[n]; 15 } 16 17 private boolean search(int i, boolean[] dp, boolean[] flag) { 18 19 if(flag[i] == true) { 20 return dp[i]; 21 } 22 23 if (i < 3) { 24 return dp[i] = (i > 0); 25 } 26 flag[i] = true; 27 //i - 1, i - 2 不全為true時,dp為true 28 dp[i] = (!search(i - 1, dp, flag)) || (!search (i - 2, dp, flag)); 29 return dp[i]; 30 } 31 }
7.2.2 Coins in a Line II
state
DP[i]表示從i到end能取到的最大value
function
當我們走到i
時,有兩種選擇
- 取
values[i]
- 取
values[i] + values[i+1]
1. 我們取了values[i]
,對手的選擇有 values[i+1]
或者values[i+1] + values[i+2]
剩下的最大總value分別為DP[i+2]
或DP[i+3]
,
對手也是理性的所以要讓我們得到最小value,
所以 value1 = values[i] + min(DP[i+2], DP[i+3])
2. 我們取了values[i]
和values[i+1]
同理 value2 = values[i] + values[i+1] + min(DP[i+3], DP[i+4])
最后
DP[I] = max(value1, value2)
非遞歸方式

1 public class Solution { 2 /** 3 * @param values: an array of integers 4 * @return: a boolean which equals to true if the first player will win 5 */ 6 public boolean firstWillWin(int[] values) { 7 // dp 表示從i到end 的最大值 8 int len = values.length; 9 // 長度小於2的時候第一個人一定獲勝 10 if(len <= 2) 11 return true; 12 int dp[] = new int[len+1]; 13 14 /*初始化,當到達最后3個數字時候,當作特例來處理 15 剩下0個元素,最大能拿到是0 16 剩下1個元素,最大能拿到是當前的那個元素 17 剩下2個元素,最大能拿到是2個都拿走 18 剩下3個元素,第三個肯定拿不到,那么拿到前2個 19 */ 20 dp[len] = 0; 21 dp[len-1] = values[len-1]; 22 dp[len-2] = values[len-1] + values[len - 2]; 23 dp[len - 3] = values[len-3] + values[len - 2]; 24 25 // 動態規划從大到小(從末尾開始,其實還是從小到大,所謂的小是剩下的硬幣個數的小到大) 26 for(int i = len - 4;i >= 0; i--){ 27 //取1個元素 28 dp[i] = values[i] + Math.min(dp[i+2],dp[i+3]); 29 //取2個元素 30 dp[i] = Math.max(dp[i],values[i]+values[i+1]+ Math.min(dp[i+3],dp[i+4])); 31 } 32 int sum = 0; 33 for(int a:values) 34 sum +=a; 35 return dp[0] > sum - dp[0]; 36 } 37 }
上面的方法略難懂,常規的記憶化搜索的方式為:
state:
dp[i] 現在還剩i個硬幣,先手最多取硬幣的價值
function:
n是所有的硬幣數目
pick_one = min(dp[i - 2], dp[i - 3]) + coin[n - i];
//為什么是coin n - i eg : 12345 n = 5 i = 3 剩下3個數,取一個數(3, index = 2)的話, index = n - i = 2;
pick_two = min(dp[i - 3], dp[i -4]) + coin[n - i] + coin[n - i + 1];
dp[i] = max(pick_one, pick_two);
initialize:
dp[0] = 0;
dp[1] = coin[length - 1];
dp[2] = coin[length -2] + coin[length - 1];
dp[3] = coin[length - 3] + coin[length - 2];
Answer:
dp[n]

1 public class Solution { 2 /** 3 * @param values: an array of integers 4 * @return: a boolean which equals to true if the first player will win 5 */ 6 public boolean firstWillWin(int[] values) { 7 int m = values.length; 8 int[] dp = new int[m + 1]; 9 boolean[] flag = new boolean[m + 1]; 10 11 int sum = 0; 12 for(int now : values) 13 sum += now; 14 15 return sum < 2*search(values.length,values, dp, flag); 16 } 17 18 private int search(int i, int[] values,int[] dp,boolean[] flag) { 19 if (flag[i] == true) { 20 return dp[i]; 21 } 22 flag[i] = true; 23 int n = values.length; 24 if (i == 0) { 25 dp[i] = 0; 26 } else if (i == 1) { 27 dp[i] = values[n - 1]; 28 } else if (i == 2) { 29 dp[i] = values[n -1] + values[n - 2]; 30 } else if (i == 3) { 31 dp[i] = values[n - 2] + values[n - 3]; 32 } else { 33 dp[i] = Math.max( 34 values[n - i] + Math.min(search(i - 2, values, dp, flag), search(i - 3, values, dp, flag)), 35 values[n - i] + values[n - i + 1] + Math.min(search(i - 3, values, dp, flag), search (i - 4, values, dp, flag))); 36 } 37 return dp[i]; 38 } 39 }
7.2.3 Coins in a Line III
for 循環的方式:

1 public class Solution { 2 /** 3 * @param values: an array of integers 4 * @return: a boolean which equals to true if the first player will win 5 */ 6 public boolean firstWillWin(int[] values) { 7 // write your code here 8 if(values.length <= 2) { 9 return true; 10 } 11 12 13 int[][] dp = new int[values.length + 3][values.length + 3]; 14 15 16 //初始化從第i個位置到第i個位置能拿到的最大值為value[i - 1] 17 for(int i= 1; i <= values.length; i++) { 18 dp[i][i] = values[i - 1]; 19 } 20 21 22 //dp[i][j] 從第i個位置到第j個位置能取到的最大值 23 int sum = 0; 24 for (int i = values.length; i >= 1; i--) { 25 sum += values[i-1]; 26 for (int j = i + 1; j <= values.length; j++) { 27 //System.out.println("debug i ==" + i+ "debug j ==" + j); 28 29 dp[i][j] = Math.max( 30 values[i - 1] + Math.min(dp[i + 2][j], dp[i + 1][j - 1]), 31 values[j - 1] + Math.min(dp[i + 1][j - 1], dp[i][j - 2])); 32 } 33 } 34 35 return dp[1][values.length] > sum - dp[1][values.length]; 36 37 } 38 }
記憶化搜索的方式:
state: dp[i][j] 現在還有第i個到第j個硬幣,現在先手取得硬幣的最高價值
function:
pick_left = min(dp[i + 2][j], dp[i + 1][j - 1]) + coin[i];
pick_right = min(dp[i][j - 2], dp[i + 1][j - 1]) + coin[j];
dp[i][j] = max(pick_left, pick_right);
intialize:
dp[i][i] = coin[i];
dp[i][i + 1] = max(coin[i], coin[i + 1]);
answer:
dp[0][n - 1]
其實代碼並不復雜!

1 public class Solution { 2 /** 3 * @param values: an array of integers 4 * @return: a boolean which equals to true if the first player will win 5 */ 6 public boolean firstWillWin(int[] values) { 7 // write your code here 8 int n = values.length; 9 int [][]dp = new int[n + 1][n + 1]; 10 boolean [][]flag =new boolean[n + 1][n + 1]; 11 12 int sum = 0; 13 for(int now : values) 14 sum += now; 15 16 return sum < 2*MemorySearch(0,values.length - 1, dp, flag, values); 17 } 18 int MemorySearch(int left, int right, int [][]dp, boolean [][]flag, int []values) { 19 20 if(flag[left][right]) 21 return dp[left][right]; 22 flag[left][right] = true; 23 if(left > right) { 24 dp[left][right] = 0; 25 } else if (left == right) { 26 dp[left][right] = values[left]; 27 } else if(left + 1 == right) { 28 dp[left][right] = Math.max(values[left], values[right]); 29 } else { 30 int pick_left = Math.min(MemorySearch(left + 2, right, dp, flag, values), MemorySearch(left + 1, right - 1, dp, flag, values)) + values[left]; 31 int pick_right = Math.min(MemorySearch(left, right - 2, dp, flag, values), MemorySearch(left + 1, right - 1, dp, flag, values)) + values[right]; 32 dp[left][right] = Math.max(pick_left, pick_right); 33 } 34 return dp[left][right]; 35 } 36 37 38 }
7.3 記憶化搜索與區間類動態規划
特點:1. 求一段區間的解max/min/count
2.轉移方程通過區間更新
3. 從大到小的更新
7.3.1 stong game
死胡同: 容易想到一個思路從小往大,枚舉第一次合並在哪? 就是說拿2顆石頭先合並,然后再繼續怎么合並,但是這樣做,重復的中間變量太多了
記憶化搜索的思路,從大到小,先考慮最后 0 -(n - 1)次的總花費
state: dp[i][j]表示把第i到第j所有石子的價值和
function: 預處理sum[i,j]表示從i到j的所有石子的價值總和
dp[i][j] = min(dp[i][k] + dp[k + 1] + sum[i,j]) 對於所有k屬於{i,j}
intialize:
for each i
dp[i][i] = 0
answer: dp[0][n - 1]

1 public class Solution { 2 /** 3 * @param A an integer array 4 * @return an integer 5 */ 6 public int stoneGame(int[] A) { 7 if (A == null || A.length == 0) { 8 return 0; 9 } 10 11 int size = A.length; 12 int[][] dp = new int[size][size]; 13 int[][] sum = new int[size][size]; 14 int[][] flag = new int[size][size]; 15 16 for (int i = 0; i < size; i++) { 17 dp[i][i] = 0; 18 flag[i][i] = 1; 19 sum[i][i] = A[i]; 20 for (int j = i + 1; j < size; j++) { 21 sum[i][j] = sum[i][j - 1] + A[j]; 22 } 23 } 24 return search(0, size - 1, dp, sum, flag); 25 } 26 private int search(int left, int right, int[][] dp, int[][] sum, int[][] flag) { 27 if (flag[left][right] == 1) { 28 return dp[left][right]; 29 } 30 31 dp[left][right] = Integer.MAX_VALUE; 32 for (int i= left; i < right; i++) { 33 dp[left][right] = Math.min(dp[left][right], search(left, i, dp, sum, flag) + search(i + 1, right, dp, sum, flag) + sum[left][right]); 34 } 35 flag[left][right] = 1; 36 return dp[left][right]; 37 } 38 }
o(n^2)的外層循環,對每個格子進行記憶化搜索,然后每個格子的區間要遍歷切分位又是一層o(n)
所以總的是o(n^3)
7.3.2 Burst Ballons
記憶化搜索的思路: 枚舉最后一個打破的氣球是哪個!
深刻理解,從最后剩下一個氣球還是遞歸,分別求左右兩邊
對於邊界情況的處理,新開了一個數組,兩邊分別放了一個1!

1 public class Solution { 2 public int maxCoins(int[] nums) { 3 if (nums == null || nums.length == 0) { 4 return 0; 5 } 6 7 int m = nums.length; 8 int[][] flag = new int[m + 2][m + 2]; 9 int[][] best = new int[m + 2][m + 2]; 10 int[] array = new int[m + 2]; 11 12 //creat a new array with 1 on two end 13 array[0] = array[m + 1] = 1; 14 for (int i = 1; i <= m; i++) { 15 array[i] = nums[i - 1]; 16 } 17 for (int i = 1; i <= m; i++) { 18 for (int j = 1; j <=m; j++) { 19 best[i][j] = search(flag, best, array, i, j); 20 } 21 } 22 return search(flag, best, array, 1, m); 23 } 24 private int search (int[][] flag, int[][] best, int[] array, int start, int end) { 25 if (flag[start][end] == 1) { 26 return best[start][end]; 27 } 28 int res = 0; 29 for (int k = start; k <= end; k++) { 30 int midValue = array[start - 1] * array[k] * array[end + 1]; 31 int leftValue = search(flag, best, array, start, k - 1); 32 int rightValue = search(flag,best,array, k + 1, end); 33 res = Math.max(res, leftValue + midValue + rightValue); 34 } 35 best[start][end] = res; 36 flag[start][end] = 1; 37 return res; 38 } 39 }
其他未分類:
8.1 unique binary search tree
其實就是枚舉每一個點作為根的時候的左右子樹的數量
首先要明確 count[i]表示有i個數的時候的子樹數量
比如總數為3的時候 (1, 2, 3)
1作為根的話, 1的左子樹只能有0個點 count[0] 1的右子樹有2個點所以count[2]
2作為根的話, 2的左子樹有1這個點, count[1], 2的右子樹只能有3這個點 所以count[1]
3作為根的話, 3的左子樹沒有點 count[0] 3的右子樹有2個點 所以 count[2]

1 public class Solution { 2 /* 3 The case for 3 elements example 4 Count[3] = Count[0]*Count[2] (1 as root) 5 + Count[1]*Count[1] (2 as root) 6 + Count[2]*Count[0] (3 as root) 7 8 Therefore, we can get the equation: 9 Count[i] = ∑ Count[0...k] * [ k+1....i] 0<=k<i-1 10 11 */ 12 public int numTrees(int n) { 13 int[] count = new int[n+2]; 14 count[0] = 1; 15 count[1] = 1; 16 17 for(int i=2; i<= n; i++){ 18 for(int j=0; j<i; j++){ 19 count[i] += count[j] * count[i - j - 1]; 20 } 21 } 22 return count[n]; 23 } 24 }
8.2 Unique Binary Search Trees II
思路是每次一次選取一個結點為根,然后遞歸求解左右子樹的所有結果,最后根據左右子樹的返回的所有子樹,依次選取然后接上(每個左邊的子樹跟所有右邊的子樹匹配,而每個右邊的子樹也要跟所有的左邊子樹匹配,總共有左右子樹數量的乘積種情況),構造好之后作為當前樹的結果返回。代碼如下:

1 public class Solution { 2 public ArrayList<TreeNode> generateTrees(int n) { 3 return generate(1, n); 4 } 5 6 private ArrayList<TreeNode> generate(int start, int end){ 7 ArrayList<TreeNode> rst = new ArrayList<TreeNode>(); 8 9 if(start > end){ 10 rst.add(null); 11 return rst; 12 } 13 14 for(int i=start; i<=end; i++){ 15 ArrayList<TreeNode> left = generate(start, i-1); 16 ArrayList<TreeNode> right = generate(i+1, end); 17 for(TreeNode l: left){ 18 for(TreeNode r: right){ 19 // should new a root here because it need to 20 // be different for each tree 21 TreeNode root = new TreeNode(i); 22 root.left = l; 23 root.right = r; 24 rst.add(root); 25 } 26 } 27 } 28 return rst; 29 } 30 }
8.3 perfect square
迷之算法= =

1 public class Solution { 2 public int numSquares(int n) { 3 int[] dp = new int[n + 1]; 4 Arrays.fill(dp, Integer.MAX_VALUE); 5 for(int i = 0; i * i <= n; i++) { 6 //1, 4, 9, 16 7 dp[i * i] = 1; 8 } 9 for (int i = 0; i <= n; ++i) 10 for (int j = 0; i + j * j <= n; ++j) 11 dp[i + j * j] = Math.min(dp[i] + 1, dp[i + j * j]); 12 13 return dp[n]; 14 } 15 }