動態規劃的實質: 根據小問題的結果來判斷大問題的結果java
何時使用動態規劃:node
何時不用動態規劃:git
動態規劃4要素 面試
面試中動態規劃類型:算法
1. 座標型動態規劃數組
state: app
f[x]表示我從起點走到座標x....less
f[x][y] 表示我從起點走到座標x,y....ide
function: 研究走到x,y這個點以前的一步oop
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:
這道題給了咱們一個正整數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:
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
DP[i]表示從i到end能取到的最大value
當咱們走到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 }