一. 理解
-
0-1揹包問題是個什麼問題
一個小偷帶了一個只能容納C的揹包來店裏偷東西,店裏有n個商品,每個商品有重量和價值,n(w,v)
問小偷能偷到的最大價值推薦一個視頻,看完了可以很好的理解什麼是揹包問題,以及解決揹包問題的思路、思想
-
在視頻裏面主要就是學會思想,實際代碼操作還是有點不同的,需要自己動手寫一篇並加以理解
/** * 使用二維數組接受每個結點的值,該節點表示容量爲w時,第k個及之前的商品的最大價值;int[k][w] * @param weight 商品重量列表 * @param value 商品價值列表,所以這裏weight和value的長度相等,一一對應 * @param cap 小偷所帶揹包的容量 * @return 小偷所能偷到的最大價值 */ public int backpack(int[] weight, int[] value, int cap) { int length = weight.length; int[][] ints = new int[length][cap + 1]; // 填充第一行,因爲後面的循環第一行無法計算到 for (int i = 0; i <= cap; i++) { ints[0][i] = i < weight[0] ? 0 : value[0]; } for (int i = 1; i < length; i++) { for (int j = 0; j <= cap; j++) { // 小偷的揹包裝不下 if (j < weight[i]) { ints[i][j] = ints[i - 1][j]; } else { // 小偷選擇裝不裝該商品的兩種情況 ints[i][j] = Math.max(ints[i - 1][j], ints[i - 1][j - weight[i]] + value[i]); } } } return ints[length - 1][cap]; }
-
優化,空間複雜度的優化:
將二維數組壓縮成一維數組,利用記憶(一維數組存儲的每行都會被替換掉)的特點
因爲由前面的公式可以知道,求 F(i,j)=max( F(i-1,j) , F(i-1,j-w(i))+value(i) ),即需要知道上一行的兩個數,j和j-w(i)
利用記憶的特點也就是,當前行的 本身 和 j-w(i) 兩個數
一維代替二維,就是一維的多次複用
還有一點需要注意的是:替換的話,遍歷每行的時候,必須從後往前,不然不會取到之前的值/** * 空間極致化,將二維數組壓縮成一維數組,利用記憶(一維數組存儲的每行都會被替換掉)的特點 * 因爲由前面的公式可以知道,求 F(i,j)=max( F(i-1,j) , F(i-1,j-w(i))+value(i) ),即需要知道上一行的兩個數 * 利用記憶的特點也就是,當前行的 本身 和 j-w(i) 兩個數 * 一維代替二維,就是一維的多次複用 * 還有一點需要注意的是:替換的話,遍歷每行的時候,必須從後往前,不然不會取到之前的值 */ public int backpackByOneArray(int[] weight, int[] value, int cap) { int length = weight.length; int[] ints = new int[cap + 1]; for (int i = 0; i < length; i++) { for (int j = cap; j >= weight[i]; j--) { ints[j] = Math.max(ints[j], ints[j - weight[i]] + value[i]); } } return ints[cap]; }
二. 應用
1. LeetCode算法題第416: 分割等和子集
給定一個只包含正整數的非空數組。是否可以將這個數組分割成兩個子集,使得兩個子集的元素和相等
問題轉換:0-1揹包問題,數組相當於商品,商品的重量和價值相等,揹包的容量就是sum/2,看能不能求得出最大價值等於揹包容量判斷有沒有指定和
/**
* 這是我自己根據揹包問題看出來的解法
*/
public boolean canPartition2(int[] nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
// 奇數
if ((sum & 1) == 1) {
return false;
}
sum = sum >> 1;
// 問題轉換:0-1揹包問題,數組相當於商品,商品的重量和價值相等,揹包的容量就是sum,看能不能求得出最大價值等於揹包容量判斷有沒有指定和
int length = nums.length;
int[] ints = new int[sum + 1];
for (int i = 0; i < length; i++) {
for (int j = sum; j >= nums[i]; j--) {
ints[j] = Math.max(ints[j], ints[j - nums[i]] + nums[i]);
}
}
return ints[sum] == sum;
}
還有一種更簡潔的寫法,
/**
* 這是我自己根據揹包問題看出來的解法
*/
public boolean canPartition2(int[] nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
// 奇數
if ((sum & 1) == 1) {
return false;
}
sum = sum >> 1;
// 問題轉換:0-1揹包問題,數組相當於商品,商品的重量和價值相等,揹包的容量就是sum,看是否可以剛好裝滿揹包
boolean[] result = new boolean[sum + 1];
// 揹包容量爲0的時候,肯定是true,因爲可以什麼都不裝(剛好裝滿)
result[0] = true;
for (int num : nums) {
for (int j = sum; j >= num; j--) {
// 容量爲j的時候,是否可以剛好裝滿,兩種情況:1.不裝當前數,2.裝當前數;兩種情況下,只需要滿足一種,即可達到要求
result[j] = result[j] || result[j - num];
}
}
return result[sum];
-
給定一個整數數組和一個整數,問能否在數組中找到任意數之和等於該整數(每個數只能用一次)
問題轉換:揹包問題,數組相當於商品,商品的價格和重量相等(爲數組的值),待求和的整數相當於揹包容量
最後:看求出來的最大價值跟揹包容量是否一致即可判斷/** * 問題轉換:揹包問題,數組相當於商品,商品的價格和重量相等(爲數組的值),待求和的整數相當於揹包容量 * 最後:看求出來的最大價值跟揹包容量是否一致即可判斷 */ public boolean ifCanFindArraySum(int[] nums, int sum) { if (nums == null || nums.length == 0) { return false; } int[] result = new int[sum + 1]; for (int num : nums) { for (int j = sum; j >= num; j--) { // 兩種情況 result[j] = Math.max(result[j], result[j - num] + num); } } return result[sum] == sum; }
其實跟LeetCode那一題很相似
第二種寫法類似 -
給定一個整數數組和一個整數,問在數組中任意數之和等於該整數的組合方式有多少種(每個數只能用一次)
轉換爲:揹包問題,揹包容量sumP,數組元素商品,元素值商品重量,求剛好裝滿揹包的商品組合數public int findTargetSumWays(int[] nums, int sum) { int[] result = new int[sum + 1]; // 和等於0有一種方式 result[0] = 1; for (int num : nums) { for (int j = sum; j >= num; j--) { // 和等於j:兩種情況,1.不需要當前數,2.需要當前數(和等於j-當前數) result[j] += result[j - num]; } } return result[sum]; }
-
揹包問題總結:
/**
* 揹包問題,其實是一類問題(要不要當前數,揹包容量轉數組),並不是侷限於求最大價值(數組形式不侷限),
* 使用的時候關鍵是要找到遞歸的條件,即要不要當前數的兩種情況該怎麼組和成結果
* 如:
* 最大價值,max(兩種情況)
* 是否可以剛好裝滿:||(兩種情況)
* 剛好裝滿的組合數:+(兩種情況)
*/
遇到時還需要靈活運用