目錄
理論基礎
?一、基礎題目
LeetCode509:斐波那契數
LeetCode70:爬樓梯
LeetCode746:使用最小花費爬樓梯
LeetCode62:不同路徑
?LeetCode63:不同路徑ii
?LeetCode343:整數拆分
?LeetCode96:不同的二叉搜索樹
二、背包問題
2.1 01背包
01背包理論基礎1:二維dp數組
01背包理論基礎2:滾動/一維dp數組
LeetCode416:分割等和子集(求是否能正好裝滿)
LeetCode1049:最后一塊石頭的重量ii(最多裝多少)
?LeetCode493:目標和(求裝滿背包有多少種方法)
LeetCode474:一和零(求裝滿背包最多有多少個物品)
2.2 完全背包
理論基礎
LeetCode518:零錢兌換ii(求組合數)
?LeetCode377:組合總和iv(求排列)
?LeetCode70:爬樓梯(求排列數)
LeetCode322:零錢兌換(求最小數)
LeetCode279:完全平方數(求最小數)
LeetCode139:單詞拆分
2.3 多重背包
理論基礎
理論基礎
動態規劃,英文:Dynamic Programming,簡稱DP,如果某一問題有很多重疊子問題,使用動態規劃是最有效的。
所以動態規劃中每一個狀態一定是由上一個狀態推導出來的;
舉例:
有N件物品和一個最多能背重量為W 的背包。第i件物品的重量是weight[i],得到的價值是value[i] 。每件物品只能用一次,求解將哪些物品裝入背包里物品價值總和最大。
動態規劃中dp[j]是由dp[j-weight[i]]推導出來的,然后取max(dp[j], dp[j - weight[i]] + value[i])。
這一點就區分于貪心,貪心沒有狀態推導,而是從局部直接選最優的。
- 狀態轉移公式(遞推公式)在動態規劃中相當重要
- 動態規劃五部曲如下:
- 確定dp數組(dp table)以及下標的含義
- 確定遞推公式
- dp數組如何初始化(很多時候遞推公式決定了dp數組要如何初始化)
- 確定遍歷順序
- 舉例推導dp數組
在debug時把dp數組打印出來,看看究竟是不是按照自己思路推導的
?一、基礎題目
LeetCode509:斐波那契數
思路:每一個數字等于前兩項之和,給定n求F(n)。動規五部曲——
①dp數組和下標的含義:dp[i]即第i個數的斐波那契數
②遞歸公式:題干中就給出了,dp[i] = dp[i - 1] + dp[i - 2];
③初始化:題干中也給出了,dp[0] = 0,dp[1] = 1;
④遍歷順序:從遞歸公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依賴 dp[i - 1] 和 dp[i - 2],那么遍歷的順序一定是從前到后遍歷的
⑤舉例推導:按照遞推公式,n為10時,dp數組應該是如下的:
0 1 1 2 3 5 8 13 21 34 55(如果結果不對,就打印dp數組)
class Solution {public int fib(int n) {if(n<=1) return n;int[] dp = new int[n+1];dp[0] = 0;dp[1] = 1;for(int i = 2; i<=n; i++){dp[i] = dp[i-1] + dp[i-2];}return dp[n];}
}
LeetCode70:爬樓梯
思路:每次你可以爬 1 或 2 個臺階
①dp數組和下標的含義:爬到第i階,有dp[i]種方法
②遞歸公式:爬到i-1階有dp[i-1]種方法,然后再爬一階就是dp[i];同理,爬到i-2階有dp[i-2]種方法,然后再爬兩階就是dp[i]。所以dp[i] = dp[i - 1] + dp[i - 2];
③初始化:可以推測出,dp[0] = 0(不重要),dp[1] = 1,dp[2] = 2;
④遍歷順序:從遞歸公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出遍歷的順序一定是從前到后遍歷的
⑤舉例推導:按照遞推公式,n為5時,dp數組應該是如下的:
0 1 2 3 5 8
發現不就是斐波那契數嗎?!同樣的代碼一點毛病沒有
public int climbStairs(int n) {int[] dp = new int[n + 1];dp[0] = 1;dp[1] = 1;for (int i = 2; i <= n; i++) {dp[i] = dp[i - 1] + dp[i - 2];}return dp[n];
}
也可以使用變量優化一下空間復雜度
class Solution {public int climbStairs(int n) {if(n <= 2) return n;int a = 1, b = 2, sum = 0;for(int i = 3; i <= n; i++){sum = a + b; // f(i - 1) + f(i - 2)a = b; // 記錄f(i - 1),即下一輪的f(i - 2)b = sum; // 記錄f(i),即下一輪的f(i - 1)}return b;}
}
LeetCode746:使用最小花費爬樓梯
思路:可以選擇從下標0或者1開始爬,爬1-2階
①dp數組和下標的含義:爬到第i階,所花費的最少體力是dp[i]
②遞歸公式:要算得dp[i]有兩種方法,一個是從dp[i-1]花cost[i-1]向上爬一階,一個是行dp[i-2]花cost[i-2]向上爬兩階,最終使用哪種是最少話費呢?
那就是dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
③初始化:可以推測出,dp[0] = 0,dp[1] = 0;
④遍歷順序:從遞歸公式中可以看出遍歷的順序一定是從前到后遍歷的
⑤舉例推導
class Solution {public int minCostClimbingStairs(int[] cost) {int len = cost.length;int[] dp = new int[len + 1];// 從下標為 0 或下標為 1 的臺階開始,因此支付費用為0dp[0] = 0;dp[1] = 0;// 計算到達每一層臺階的最小費用for (int i = 2; i <= len; i++) {dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);}return dp[len];}
}
LeetCode62:不同路徑
思路:如果采用深度搜索,則這棵樹的深度為m+n-1,而深搜代碼的時間復雜度為O(2^(m + n - 1) - 1),這是非常大的。那么采用動態規劃,從(0,0)出發,到(m,n):
①dp數組和下標的含義:dp[i][j]表示從(0,0)出發,到達(i,j)有多少種路徑;
②遞歸公式:要算得dp[i][j]有兩種方法,一個是從dp[i-1][j] 向下移動一格,一個是從dp[i][j-1]向右移動一格。dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
③初始化:首先dp[i][0]一定都是1,因為從(0, 0)的位置到(i, 0)的路徑只有一條,那么dp[0][j]也同理
for (int i = 0; i < m; i++) dp[i][0] = 1; for (int j = 0; j < n; j++) dp[0][j] = 1;
④遍歷順序:從遞歸公式中可以看出遍歷的順序是從左到右一層一層遍歷的
⑤舉例推導
class Solution {public int uniquePaths(int m, int n) {int[][] dp = new int[m][n];for(int i=0;i<m;i++){dp[i][0] = 1;}for(int i=0;i<n;i++){dp[0][i] = 1;}for(int i=1;i<m;i++){for(int j=1;j<n;j++){dp[i][j] = dp[i-1][j] + dp[i][j-1];}}return dp[m-1][n-1];}
}
?LeetCode63:不同路徑ii
思路:相比1,給出了障礙物,路徑需要繞開障礙物。與1的區別僅僅在于:
1. 初始化時如果障礙在邊上,則僅初始化障礙前的dp值為1,障礙后仍為0(無法到達)
2. 在遍歷時跳過給有障礙的dp賦值
class Solution {public int uniquePathsWithObstacles(int[][] obstacleGrid) {int m = obstacleGrid.length;int n = obstacleGrid[0].length;int[][] dp = new int[m][n];for(int i=0;i<m && obstacleGrid[i][0] == 0;i++){dp[i][0] = 1;}for(int i=0;i<n && obstacleGrid[0][i] == 0;i++){dp[0][i] = 1;}for(int i=1;i<m;i++){for(int j=1;j<n;j++){if(obstacleGrid[i][j] == 1) continue;dp[i][j] = dp[i-1][j] + dp[i][j-1];}}return dp[m-1][n-1];}
}
?LeetCode343:整數拆分
思路:給定一個正整數?n,將其拆分為至少兩個正整數的和,并使這些整數的乘積最大化。?
①dp數組和下標的含義:分拆數字i,所得到的最大乘積是dp[i]
②遞歸公式:要算得dp[i]有兩種方法,從1遍歷j,一個是j*(i-j),一個是j*dp[i-j]。可以這么理解,j * (i - j) 是單純的把整數拆分為兩個數相乘,而j * dp[i - j]是拆分成兩個以及兩個以上的個數相乘。
那就是dp[i] =?max({dp[i], (i - j) * j, dp[i - j] * j});
在取最大值的時候,為什么還要比較dp[i]呢?因為在遞推公式推導的過程中,每次計算dp[i],取最大的而已。
③初始化:可以推測出,dp[2] = 1,dp[3] = 2;(n從3開始拆)
④遍歷順序:從遞歸公式中可以看出遍歷的順序一定是從前到后遍歷的,且先有dp[i - j]再有dp[i]。i從3-n,j從1到i-1(不包括i-1),更優化的遍歷方法是:j直接從1到i/2,這是因為一定是拆分成m個近似相同的子數相乘才是最大的
⑤舉例推導
class Solution {public int integerBreak(int n) {int[] dp = new int[n+1];dp[2] = 1;for(int i = 3;i<=n;i++){for(int j = 1;j<=i/2;j++){dp[i] = Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));}}return dp[n];}
}
?LeetCode96:不同的二叉搜索樹
思路:重點是推導公式,容易看出
dp[3],就是 元素1為頭結點搜索樹的數量 + 元素2為頭結點搜索樹的數量 + 元素3為頭結點搜索樹的數量,
元素1為頭結點搜索樹的數量 = 右子樹有2個元素的搜索樹數量 * 左子樹有0個元素的搜索樹數量
元素2為頭結點搜索樹的數量 = 右子樹有1個元素的搜索樹數量 * 左子樹有1個元素的搜索樹數量
元素3為頭結點搜索樹的數量 = 右子樹有0個元素的搜索樹數量 * 左子樹有2個元素的搜索樹數量
即dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]
① dp的概念:dp[i]就是以1到i為節點構成的二叉搜索樹的個數
②遞歸公式:i從2開始遍歷到n,j從1開始遍歷到i,dp[i] += dp[j-1]*dp[i-j];
③初始化:dp[0]=1; dp[1] = 1;
④遍歷順序和舉例推導
class Solution {public int numTrees(int n) {//初始化 dp 數組int[] dp = new int[n + 1];//初始化0個節點和1個節點的情況dp[0] = 1;dp[1] = 1;for (int i = 2; i <= n; i++) {for (int j = 1; j <= i; j++) {//對于第i個節點,需要考慮1作為根節點直到i作為根節點的情況,所以需要累加//一共i個節點,對于根節點j時,左子樹的節點個數為j-1,右子樹的節點個數為i-jdp[i] += dp[j - 1] * dp[i - j];}}return dp[n];}
}
二、背包問題
2.1 01背包
01背包理論基礎1:二維dp數組
(物品下標-背包容量)
背包問題有多種背包方式,常見的有:01背包、完全背包、多重背包、分組背包和混合背包等等。
一個商品如果可以重復多次放入是完全背包,而只能放入一次是01背包,寫法還是不一樣的
有n件物品和一個最多能背重量為w 的背包。第i件物品的重量是weight[i],得到的價值是value[i] 。每件物品只能用一次,求解將哪些物品裝入背包里物品價值總和最大。
二維數組 01背包:
- dp[i][j] 表示從下標為[0-i]的物品里任意取,放進容量為j的背包,價值總和最大是多少。
- 遞歸公式:①不放物品i:由dp[i - 1][j]推出,即背包容量為j,里面不放物品i的最大價值,此時dp[i][j]就是dp[i - 1][j]。(其實就是當物品i的重量大于背包j的重量時,物品i無法放進背包中,所以背包內的價值依然和前面相同)。②放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 為背包容量為j - weight[i]的時候不放物品i的最大價值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的價值),就是背包放物品i得到的最大價值。
- ?dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);?
- 初始化:關于初始化,一定要和dp數組的定義吻合,否則到遞推公式的時候就會越來越亂。根據遞推公式可以看出需要得知dp[i][0]和dp[0][j]的初始值,才能開始遞推。可以一開始就統一把dp數組統一初始為0。當背包容量為0時,無論怎么選取物品,都無法放入背包中,即價值為0,dp[i][0] = 0;當放入序號為0的物品時且背包容量為0-j時,所能放下物品的最大價值:當j<weight[0]時,背包里放不下物品0,dp[0][j]應該是0;當j>=weight[0]時,背包里可以放下編號0物品,dp[0][j]應該是value[0]。
- 遍歷順序:問題來了,先遍歷物品還是先遍歷背包重量呢?其實都可以!! 但是先遍歷物品更好理解。
- 舉例推導:自己推導一下
例題:卡碼網46 攜帶研究材料(給定背包容量,求裝滿的最大價值)
public class Main {public static void main(String[] args) {int[] weight = {2,2,3,1,5,2};int[] value = {2,3,1,5,4,3};int bagSize = 1;testWeightBagProblem(weight,value,bagSize);}public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){// 創建dp數組int goods = weight.length; // 獲取物品的數量int[][] dp = new int[goods][bagSize + 1];// 初始化dp數組// 創建數組后,其中默認的值就是0for (int j = weight[0]; j <= bagSize; j++) {dp[0][j] = value[0];}// 填充dp數組for (int i = 1; i < weight.length; i++) {for (int j = 1; j <= bagSize; j++) {if (j < weight[i]) {/*** 當前背包的容量都沒有當前物品i大的時候,是不放物品i的* 那么前i-1個物品能放下的最大價值就是當前情況的最大價值*/dp[i][j] = dp[i-1][j];} else {/*** 當前背包的容量可以放下物品i* 那么此時分兩種情況:* 1、不放物品i* 2、放物品i* 比較這兩種情況下,哪種背包中物品的最大價值最大*/dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);}}}System.out.println(dp[goods-1][bagSize]);}
}
01背包理論基礎2:滾動/一維dp數組
對于背包問題其實狀態都是可以壓縮的。
在使用二維數組的時候,遞推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
其實可以發現如果把dp[i - 1]那一層拷貝到dp[i]上,表達式完全可以是:
dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
即重復利用上一層,與其把dp[i - 1]這一層拷貝到dp[i]上,不如只用一個一維數組了,只用dp[j](一維數組,也可以理解是一個滾動數組)。
- 在一維數組中,dp[j]代表:容量為j的背包,所背的物品價值可以最大為dp[j];
- 遞推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);仍然相當于其中一層的不放物品i和放物品i
- 初始化:看一下遞推公式,可知只需初始化dp[0]]=0即可;
- 遍歷順序:重點是倒序遍歷,是為了確保物品i只被放入一次。二維的就不用倒序,因為二維dp[i][j]計算是通過上一層dp[i - 1][j]得來的,不會覆蓋,而由于一維數組用的是同一個dp[j],所以會從前向后覆蓋,影響dp值的計算,所以為了避免重復需要倒序。
for(int i = 0; i < weight.size(); i++) { // 遍歷物品for(int j = bagWeight; j >= weight[i]; j--) { // 遍歷背包容量dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);}
}
public class Main {public static void main(String[] args) {int[] weight = {2,2,3,1,5,2};int[] value = {2,3,1,5,4,3};int bagSize = 1;testWeightBagProblem(weight,value,bagSize);}public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){// 創建dp數組int goods = weight.length; // 獲取物品的數量int[] dp = new int[bagSize + 1];// 初始化dp數組,直接默認全為0// 填充dp數組,放不下的即j < weight[i]就默認不改變,一維數組直接繼承上一層無需賦值,簡化了代碼for (int i = 0; i < goods; i++) {for (int j = bagSize; j >= weight[i]; j--) {dp[j] = Math.max(dp[j] , dp[j-weight[i]] + value[i]);}}System.out.println(dp[bagSize]);}
}
總結:?
1. 為什么二維背包兩個for循環的嵌套順序這么寫?反過來寫行不行?再講一講初始化的邏輯。
答:兩個for循環的順序無非是先遍歷物品還是先遍歷背包容量,在二維數組中其實都行得通,但是遍歷物品更好理解。根據遞推公式,每一個dp[i][j]其實都是由上一個dp[i-1][j]或者上一個dp[i - 1][j - weight[i]] 推導出來的,如果以表格的形式來看,位于dp[i][j]的左上角,如果先遍歷物品,就是在每一個物品都遍歷一遍不同的背包重量,從左到右,從上到下;而如果先遍歷背包容量就是先從上到下,在從左到右,由于數據位于左上角,所以兩個for的順序都不影響dp[i][j]的推導。而這種推導方式顯然要得到所有的dp[0][j]和dp[i][0]公式才能接著往下,往右推導。
2. 改成一維以后,兩個for循環的順序反過來寫行不行?為什么?
答:不可以,改成一維之后只能先遍歷物品再遍歷背包容量,這是由于一維dp的寫法中背包容量一定是要倒序遍歷,其本質上還是對二維數組的遍歷,右下角的值依賴上一層左上角的值,因此需要保證左邊的值仍然是上一層的,從右向左覆蓋,保證每個物品只添加一次。如果非要換順序,倒序遍歷的作用便失去了。
LeetCode416:分割等和子集(求是否能正好裝滿)
思路:是否可以將數組分割成兩個子集,使得子集和相等。
怎么轉換成背包問題呢?
首先,每個元素我們只用一次,所以是01背包問題。背包的最大容量是target = sum/2,放入n個元素,每個元素的值既是他的value,也是他的weight;dp[j]的定義是容量為j的背包里能裝的最大重量,如果正好裝滿,即dp[j] = j。
其次,01背包的遞推公式為:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);對于本題,相當于背包里放入數值,那么物品i的重量是nums[i],其價值也是nums[i]。所以遞推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);01背包的初始化:dp[0] = 0即可。
首先得到背包,最終做判斷如果dp[target] = target,就說明可以分割等和子集。
class Solution {public boolean canPartition(int[] nums) {int sum = 0;for(int i= 0;i<nums.length;i++){sum += nums[i];}if(sum % 2 == 1) return false;int target = sum/2;int[] dp = new int[target+1];for(int i = 0;i<nums.length;i++){for(int j = target;j>=nums[i];j--){dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);}}if(dp[target] == target){return true;}return false;}
}
LeetCode1049:最后一塊石頭的重量ii(最多裝多少)
思路:其實本質上就是盡量讓石頭分成重量相同的兩堆,相撞之后剩下的石頭最小,這樣就化解成01背包問題了,同樣是每個物品重量和價值都為stones[i]。target = sum/2。
得到背包后,不用完全相等,一堆石頭是dp[target],另一堆是sum - dp[target]。由于target = sum/2是向下取整,所以sum - dp[target] >?dp[target],
所以最終的最小石頭重量為?(sum - dp[target]) - dp[target]。
class Solution {public int lastStoneWeightII(int[] nums) {int sum = 0;for(int i= 0;i<nums.length;i++){sum += nums[i];}int target = sum/2;int[] dp = new int[target+1];for(int i = 0;i<nums.length;i++){for(int j = target;j>=nums[i];j--){dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);}}return sum - 2*dp[target];}
}
?LeetCode493:目標和(求裝滿背包有多少種方法)
思路:如何轉化成01背包問題——
一部分left為+,一部分right為-,left-right = target,left+rght = sum,已知sum和target,就可以求出?left = (target + sum)/2 ,此時問題就變成了在集合中找bagSize = left的組合。
之前的幾道題都是求容量為j的背包,最多能裝多少,本題則是說裝滿有幾種方法,是組合問題。
* 首先排除兩種情況,即①target的絕對值>sum,以及②(target+sum)除以2的余數不為0
1. dp[j]:容量為j的背包能有dp[j]種方法,
2. 遞推公式:求方法的總和——dp[j] += dp[j - nums[i]]
3. 初始化:dp[0] = 1,別的初始化為0;背包容量為0的話,[0]可以有一種取法,[0,0,0,0,0]有32種取法,也是dp[0] = 1疊加起來的
4. 遍歷順序就是01背包的先遍歷物品,再倒序遍歷背包容量
5. 舉例推導:
這道題也可以用回溯來做,甚至可以列出所有組合,dp僅僅可以求個數
class Solution {public int findTargetSumWays(int[] nums, int target) {int sum =0;for(int i=0;i<nums.length;i++){sum += nums[i];}if(Math.abs(target) > sum) return 0;if((target+sum)%2 == 1) return 0;int bagSize = (target+sum)/2;int[] dp = new int[bagSize+1];dp[0] = 1;for(int i=0;i<nums.length;i++){for(int j=bagSize;j>=nums[i];j--){dp[j] += dp[j-nums[i]];}}return dp[bagSize];}
}
LeetCode474:一和零(求裝滿背包最多有多少個物品)
思路:strs數組里的元素就相當于是物品,包含zeroNum個0,oneNum個1,這就是一個典型的01背包!?只不過物品的重量有了兩個維度而已。
dp[i][j]就相當于最多有i個0和j個1的最大子集大小,遞推通過加一來實現即可。字符串的zeroNum和oneNum相當于物品的重量(weight[i]),字符串本身的個數相當于物品的價值(value[i])。
相當于還是先遍歷物品,在內循環里通過構建zeroNum和oneNum兩個雙循環來遍歷背包容量。
01背包遞推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
該二維weight遞推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
其實就是把dp變成二維的,本質還是一樣的
class Solution {public int findMaxForm(String[] strs, int m, int n) {int[][] dp = new int[m+1][n+1];for(String str: strs){ //遍歷物品int zeroNum = 0;int oneNum = 0;//得到每個物品的weight,分為0和1兩個維度for(char ch: str.toCharArray()){if(ch == '0'){zeroNum++;}else{oneNum++;}}for(int i = m; i>=zeroNum; i--){for(int j = n; j>=oneNum; j--){dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);}}}return dp[m][n];}
}
2.2 完全背包
理論基礎
區別于01背包:每件物品都可以放入背包無限次,可以重復放入
首先再回顧一下01背包的核心代碼
for(int i = 0; i < weight.size(); i++) { // 遍歷物品for(int j = bagWeight; j >= weight[i]; j--) { // 遍歷背包容量dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);}
}
我們知道01背包內嵌的循環是從大到小遍歷,為了保證每個物品僅被添加一次。
而完全背包的物品是可以添加多次的,所以要從小到大去遍歷,即:
// 先遍歷物品,再遍歷背包
for(int i = 0; i < weight.size(); i++) { // 遍歷物品for(int j = weight[i]; j <= bagWeight ; j++) { // 遍歷背包容量dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);}
}
?且在純完全背包中,對于一維dp數組來說,其實兩個for循環嵌套順序是無所謂的!
例題:攜帶研究材料(完全背包)
public class Main {public static void main(String[] args) {int[] weight = {1,2,3,4};int[] value = {2,4,4,5};int bagSize = 5;// 創建dp數組int goods = weight.length; // 獲取物品的數量int[] dp = new int[bagSize + 1];// 初始化dp數組,直接默認全為0// 填充dp數組,放不下的即j < weight[i]就默認不改變,一維數組直接繼承上一層無需賦值,簡化了代碼for (int i = 0; i < goods; i++) {for (int j = weight[i]; j <= bagSize; j++) {dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);}}System.out.println(dp[bagSize]);}
}
LeetCode518:零錢兌換ii(求組合數)
不同面額的硬幣(可以重復使用)和一個總金額,求組合數
一個關鍵的知識點:
如果求組合數就是外層for循環遍歷物品,內層for遍歷背包。
如果求排列數就是外層for遍歷背包,內層for循環遍歷物品。
class Solution {public int change(int amount, int[] coins) {int[] dp = new int[amount+1];dp[0] = 1;for(int i=0;i<coins.length;i++){for(int j=coins[i];j<=amount;j++){dp[j] += dp[j-coins[i]];}}return dp[amount];}
}
?LeetCode377:組合總和iv(求排列)
思路:同上,只是調一個for的順序,順序不同的序列被視作不同的組合所以是求排列
class Solution {public int combinationSum4(int[] nums, int target) {int[] dp = new int[target+1];dp[0] = 1;for(int i=0;i<= target;i++){for(int j=0;j<nums.length;j++){if(i >= nums[j]) dp[i] += dp[i-nums[j]];}}return dp[target];}
}
?LeetCode70:爬樓梯(求排列數)
思路:這其實就是完全背包的求排列數,秒啦
import java.util.Scanner;
public class Main{public static void main(String[] args){Scanner sc = new Scanner(System.in);while(sc.hasNextInt()){int n = sc.nextInt();int m = sc.nextInt();int[] dp = new int[n+1];dp[0] = 1;for(int i = 1;i<=n;i++){for(int j = 1;j<=m;j++){if(i>=j) dp[i] += dp[i-j];}}System.out.println(dp[n]);}}
}
LeetCode322:零錢兌換(求最小數)
湊成目標的最小硬幣數量
思路:相比于LeetCode474:一和零是01背包求最大個數(二維weight)
這道題只不過改成了是完全背包求最小個數(weight就是數值)
這個初始化dp[0]=0,且由于求的是最小值,所以需要把所有dp一開始初始化為max才能進行比較
不多說了上代碼!
class Solution {public int coinChange(int[] coins, int amount) {int max = Integer.MAX_VALUE;int[] dp = new int[amount+1];//初始化dp數組為最大值for (int j = 0; j < dp.length; j++) {dp[j] = max;}dp[0] = 0;for(int i=0;i<coins.length;i++){for(int j=coins[i];j<=amount;j++){//只有dp[j-coins[i]]不是初始最大值時,該位才有選擇的必要if(dp[j-coins[i]]!= max) dp[j] = Math.min(dp[j],dp[j-coins[i]]+1);}}return dp[amount]==max?-1:dp[amount];}
}
LeetCode279:完全平方數(求最小數)
思路:其實就是元素1-10的平方,給定target,上道題稍微改動一下就能過
class Solution {public int numSquares(int n) {int max = Integer.MAX_VALUE;int[] dp = new int[n+1];//初始化dp數組為最大值for (int j = 0; j < dp.length; j++) {dp[j] = max;}dp[0] = 0;for(int i=1;i<=10;i++){for(int j=i*i;j<=n;j++){//這個if是為了防止湊不成,而因為有1所以一定可以湊成,所以if可以略去if(dp[j-i*i]!= max) dp[j] = Math.min(dp[j],dp[j-i*i]+1);}}return dp[n]==max?-1:dp[n];}
}
LeetCode139:單詞拆分
判斷單詞是否可以被空格拆分為字典里的詞
思路:關于單詞拆分,回溯法也能做(這個再說);如何轉換成背包呢?單詞即物品,字符串即背包,這道題就轉換成了物品是否能正好把背包裝滿。
1. dp[i]代表i前面的子串是否能由字典構成,true/false
2. 遞推:如果dp[j]==true && [j,i]子串也屬于字典則dp[i]==true(i<j)
3. 初始化:dp[0] =?true
class Solution {public boolean wordBreak(String s, List<String> wordDict) {boolean[] dp = new boolean[s.length()+1];HashSet<String> set = new HashSet<>(wordDict);dp[0] = true;for(int i=1;i<=s.length();i++){for(int j=0;j<i;j++){if(dp[j] == true && set.contains(s.substring(j,i))){//本身子字符串就不包含i,所以i就是下一個單詞開始的第一個字符dp[i] = true;}}}return dp[s.length()];}
}
2.3 多重背包
理論基礎
相當于n種物品和一個容量為v的背包,每種物品只有mi件,耗費空間ci,價值是wi
多重背包攤開可以變成01背包
例題:卡碼網56 攜帶礦石資源
import java.util.Scanner;
class multi_pack{public static void main(String [] args) {Scanner sc = new Scanner(System.in);/*** bagWeight:背包容量* n:物品種類*/int bagWeight, n;//獲取用戶輸入數據,中間用空格隔開,回車鍵換行bagWeight = sc.nextInt();n = sc.nextInt();int[] weight = new int[n];int[] value = new int[n];int[] nums = new int[n];for (int i = 0; i < n; i++) weight[i] = sc.nextInt();for (int i = 0; i < n; i++) value[i] = sc.nextInt();for (int i = 0; i < n; i++) nums[i] = sc.nextInt();int[] dp = new int[bagWeight + 1];//先遍歷物品再遍歷背包,作為01背包處理for (int i = 0; i < n; i++) {for (int j = bagWeight; j >= weight[i]; j--) {//遍歷每種物品的個數for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) {dp[j] = Math.max(dp[j], dp[j - k * weight[i]] + k * value[i]);}}}System.out.println(dp[bagWeight]);}
}