01揹包問題,你該瞭解這些!
題目連結:46. 攜帶研究材料(第六期模擬筆試) (kamacoder.com)
思路:第一次遇到揹包問題,好好記住吧。程式碼隨想錄 (programmercarl.com)
#include<bits/stdc++.h>
using namespace std;
int main(){
int m,n;
cin>>m>>n;
vector<int>z(m);
vector<int>value(m);
for(int i=0;i<m;i++){
cin>>z[i];
}
for(int i=0;i<m;i++){
cin>>value[i];
}
vector<vector<int>> dp(m,vector<int>(n+1,0));
for(int i=z[0];i<=n;i++){
dp[0][i]=value[0];
}
for(int i=1;i<m;i++){
for(int j=0;j<=n;j++){
if(j<z[i])dp[i][j]=dp[i-1][j];
else dp[i][j]=max(dp[i-1][j],dp[i-1][j-z[i]]+value[i]);
}
}
cout<<dp[m-1][n];
}
01揹包問題,你該瞭解這些! 滾動陣列
滾動陣列,說白了就是將上一題中的二維陣列壓縮成一維陣列。dp陣列的更新方式變為dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
但有一點需要注意,這裡我直接引用官網原文:
這裡大家發現和二維dp的寫法中,遍歷揹包的順序是不一樣的!
二維dp遍歷的時候,揹包容量是從小到大,而一維dp遍歷的時候,揹包是從大到小。
為什麼呢?
倒序遍歷是為了保證物品i只被放入一次!。但如果一旦正序遍歷了,那麼物品0就會被重複加入多次!
舉一個例子:物品0的重量weight[0] = 1,價值value[0] = 15
如果正序遍歷
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30
此時dp[2]就已經是30了,意味著物品0,被放入了兩次,所以不能正序遍歷。
為什麼倒序遍歷,就可以保證物品只放入一次呢?
倒序就是先算dp[2]
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp陣列已經都初始化為0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
所以從後往前迴圈,每次取得狀態不會和之前取得狀態重合,這樣每種物品就只取一次了。
同時,只能先遍歷物品,巢狀倒序遍歷揹包容量,不能反著來。
void test_1_wei_bag_problem() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
// 初始化
vector<int> dp(bagWeight + 1, 0);
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]);
}
}
cout << dp[bagWeight] << endl;
}
分割等和子集
題目連結:416. 分割等和子集 - 力扣(LeetCode)
思路:首先來看我最初的思路,先將陣列排序,然後設一個堆疊來記錄我選擇了哪些元素。從小到大遍歷陣列,同時累加壓入堆疊的元素,如果累加值過大於目標值,則將棧頂彈出同時繼續嘗試加入該元素,否則更新累加值並加入堆疊(注意此時可以return的情況)。實際上,這是一種貪心解法,並不能解決這個問題,具體來說,是因為下一個元素並不能決定這個元素是否該留在這個堆疊中,而是該元素之後的所有元素共同決定這個元素能否留在堆疊中(這應該就是貪心和DP的區別所在吧)。下面是錯誤做法的核心部分(與前文略有差異,該做法從大到小遍歷陣列):
r.push(nums.back());
int z = nums.size() - 1;
while (!z) {
for (int j = z; j >= 0; j--) {
if (sum - nums[j] > 0) {
sum -= nums[j];
r.push(nums[j]);
} else if (sum - nums[j] < 0 && (!r.empty())) {
r.pop();
j++;
} else if ((sum - nums[j]) == 0) {
return true;
}
}
z--;
}
該方法不能在走投無路的時回溯到錯誤還沒有發生的時刻,也就不能解決問題。不得不說,這道題讓我對貪心和DP的理解更加深入了。
正確做法如下:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
// dp[i]中的i表示揹包內總和
// 題目中說:每個陣列中的元素不會超過 100,陣列的大小不會超過 200
// 總和不會大於20000,揹包最大隻需要其中一半,所以10001大小就可以了
vector<int> dp(10001, 0);
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
}
// 也可以使用庫函式一步求和
// int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % 2 == 1) return false;
int target = sum / 2;
// 開始 01揹包
for(int i = 0; i < nums.size(); i++) {
for(int j = target; j >= nums[i]; j--) { // 每一個元素一定是不可重複放入,所以從大到小遍歷
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
// 集合中的元素正好可以湊成總和target
if (dp[target] == target) return true;
return false;
}
};