動態規劃之6:揹包問題
阿新 • • 發佈:2020-08-29
揹包問題
揹包問題是一種組合優化的NP 完全問題:有N 個物品和容量為W的揹包,每個物品都有自己的體積w 和價值v,求拿哪些物品可以使得揹包所裝下物品的總價值最大。如果限定每種物品只能選擇0 個或1 個,則問題稱為0-1 揹包問題;如果不限定每種物品的數量,則問題稱為無界揹包問題或完全揹包問題。
0-1 揹包
我們可以定義一個二維陣列 dp 儲存最大價值,其中 dp[i][j]
表示前 i 件物品體積不超過 j 的情況下能達到的最大價值。在我們遍歷到第 i 件物品時,在當前揹包總容量為 j 的情況下,如果我們不將物品 i 放入揹包,那麼dp[i][j]= dp[i-1][j]
,即前 i 個物品的最大價值等於只取前 i-1 個物品時的最大價值;如果我們將物品 i 放入揹包,假設第 i 件物品體積為w,價值為v,那麼我們得到 p[i][j] = dp[i-1][j-w] + v
int knapsack(vector<int> weights, vector<int> values, int N, int W) { vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0)); for (int i = 1; i <= N; ++i) { int w = weights[i - 1], v = values[i - 1]; for (int j = 1; j <= W; ++j) { if (j >= w) { dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w] + v); } else { dp[i][j] = dp[i - 1][j]; } } } return dp[N][W]; }
進一步可以進行空間壓縮:
int knapsack(vector<int> weights, vector<int> values, int N, int W) { vector<int> dp(W + 1, 0); for (int i = 1; i <= N; ++i) { int w = weights[i - 1], v = values[i - 1]; for (int j = W; j >= w; --j) { dp[j] = max(dp[j], dp[j - w] + v); } } return dp[W]; }
完全揹包
在完全揹包問題中,一個物品可以拿多次。完全揹包問題的狀態轉移方程:dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v)
,其與 0-1 揹包問題的差別僅僅是把狀態轉移方程中的第二個 i-1 變成了 i。
int knapsack(vector<int> weights, vector<int> values, int N, int W)
{
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= N; ++i) {
int w = weights[i - 1], v = values[i - 1];
for (int j = 1; j <= W; ++j)
{
if (j >= w) {
dp[i][j] = max(dp[i - 1][j], dp[i][j - w] + v);
}
else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[N][W];
}
狀態空間壓縮:
int knapsack(vector<int> weights, vector<int> values, int N, int W)
{
vector<int> dp(W + 1, 0);
for (int i = 1; i <= N; ++i)
{
int w = weights[i - 1], v = values[i - 1];
for (int j = w; j <= W; ++j) {
dp[j] = max(dp[j], dp[j - w] + v);
}
}
return dp[W];
}
分割等和子集
本題可以等價於 0-1 揹包問題,設所有數字的和為 sum,我們的目標是選出一部分使得他們的和為 sum/2。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(),nums.end(),0);
if(sum % 2) return false; //@ 如果和是奇數,不可能平分
int target = sum / 2,n = nums.size();
vector<vector<bool>> dp(n+1,vector<bool>(target+1,false));
for(int i=0;i<=n;++i)
dp[i][0] = true;
for(int i=1;i<=n;++i)
for(int j=nums[i-1];j<=target;++j)
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];
return dp[n][target];
}
};
也可以對本題進行空間壓縮。注意對數字和的遍歷需要逆向。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(),nums.end(),0);
if(sum % 2) return false; //@ 如果和是奇數,不可能平分
int target = sum / 2,n = nums.size();
vector<bool> dp(target+1,false));
dp[0] = true;
for(int i=1;i<=n;++i)
for(int j=target;j>=nums[i-1];--j)
dp[j] = dp[j] || dp[j-nums[i-1]];
return dp[target];
}
};
一和零
這是一個多維費用的 0-1 揹包問題,有兩個揹包大小, 0 的數量和 1 的數量。我們在這裡直
接展示三維空間壓縮到二維後的寫法。
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
for(const string & str : strs)
{
auto [count0,count1] = count(str);
for(int i=m;i>=count0;--i)
for(int j=n;j>=count1;--j)
dp[i][j] = max(dp[i][j],1+dp[i-count0][j-count1]);
}
return dp[m][n];
}
//@ 輔助函式,統計0,1的數量
pair<int,int> count(const string& s)
{
int count0 = s.length(),count1 = 0;
for(const char &c : s)
{
if(c == '1')
{
++count1;
--count0;
}
}
return make_pair(count0,count1);
}
};
零錢兌換
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
if (coins.empty() || amount <= 0)
return 0;
int MAX = INT_MAX -1; //@ 防止 INT_MAX + 1 溢位
vector<int> dp(amount + 1, MAX);
dp[0] = 0;
for (int i = 1; i <= amount; ++i)
for(auto coin : coins)
{
if(coin <= i)
dp[i] = min(dp[i], dp[i -coin] + 1);
}
return dp.back() == MAX ? -1 : dp.back();
}
};
零錢兌換 II
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount+1,0);
dp[0] = 1;
for(auto coin : coins)
for(int i = coin;i<=amount;++i)
dp[i] += dp[i-coin];
return dp[amount];
}
};