day35 動態規劃part03
1. 01背包問題 二維 卡碼網第46題
01 背包:有n件物品和一個最多能背重量為w 的背包。第i件物品的重量是weight[i],得到的價值是value[i] 。每件物品只能用一次,求解將哪些物品裝入背包里物品價值總和最大。
動規五部曲分析:具體可看代碼隨想錄完整講解
(1)確定dp
數組以及下標的含義
dp[i][j]
表示從下標為[0-i]的物品里任意取,放進容量為j的背包,價值總和最大是多少。
(2)確定遞推公式
- 不放物品i:背包容量為j,里面不放物品i的最大價值是
dp[i - 1][j]
。 - 放物品i:背包空出物品i的容量后,背包容量為
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]);
(3)dp
數組如何初始化
首先從dp[i][j]
的定義出發,如果背包容量j
為0的話,即dp[i][0]
,無論是選取哪些物品,背包價值總和一定為0。
其次,狀態轉移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
可以看出i
是由 i-1
推導出來,那么i
為0的時候就一定要初始化。
dp[0][j]
,即:i
為0,存放編號0的物品的時候,各個容量的背包所能存放的最大價值。
那么很明顯當 j < weight[0]
的時候,dp[0][j]
應該是 0,因為背包容量比編號0的物品重量還小。
當j >= weight[0]
時,dp[0][j]
應該是value[0],因為背包容量放足夠放編號0物品。
for (int j = weight[0]; j <= bagweight; j++) {//其他部分不需要初始化,因為一開始已經都置為0了
dp[0][j] = value[0];
}
(4)確定遍歷順序
先遍歷 物品還是先遍歷背包重量呢?
都可以!! 但是先遍歷物品更好理解。
// weight數組的大小 就是物品個數
for(int i = 1; i < weight.size(); i++) { // 遍歷物品for(int j = 0; j <= bagweight; j++) { // 遍歷背包容量if (j < weight[i]) dp[i][j] = dp[i - 1][j];else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);}
}
(5)舉例推導dp
數組
#include <bits/stdc++.h>
using namespace std;int main() {int n, bagweight;// bagweight代表行李箱空間cin >> n >> bagweight;vector<int> weight(n, 0); // 存儲每件物品所占空間vector<int> value(n, 0); // 存儲每件物品價值for(int i = 0; i < n; ++i) {cin >> weight[i];}for(int j = 0; j < n; ++j) {cin >> value[j];}// dp數組, dp[i][j]代表行李箱空間為j的情況下,從下標為[0, i]的物品里面任意取,能達到的最大價值vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));// 初始化, 因為需要用到dp[i - 1]的值// j < weight[0]已在上方被初始化為0// j >= weight[0]的值就初始化為value[0]for (int j = weight[0]; j <= bagweight; j++) {dp[0][j] = value[0];}for(int i = 1; i < weight.size(); i++) { // 遍歷科研物品for(int j = 0; j <= bagweight; j++) { // 遍歷行李箱容量if (j < weight[i]) dp[i][j] = dp[i - 1][j]; // 如果裝不下這個物品,那么就繼承dp[i - 1][j]的值else {dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);}}}cout << dp[n - 1][bagweight] << endl;return 0;
}
2. 01背包問題 一維
(1)確定dp
數組的定義
在一維dp
數組中,dp[j]
表示:容量為j
的背包,所背的物品價值可以最大為dp[j]
。
(2)一維dp
數組的遞推公式
二維dp
數組的遞推公式為: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
一維dp
數組,其實就上一層 dp[i-1]
這一層 拷貝的到 dp[i]
來。
所以在 上面遞推公式的基礎上,去掉i
這個維度就好。
遞推公式為:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
以下為分析:
dp[j]
為 容量為j
的背包所背的最大價值。
dp[j]
可以通過dp[j - weight[i]]
推導出來,dp[j - weight[i]]
表示容量為j - weight[i]
的背包所背的最大價值。
dp[j - weight[i]] + value[i]
表示 容量為 [j - 物品i重量]
的背包 加上 物品i
的價值。(也就是容量為j
的背包,放入物品i
了之后的價值即:dp[j]
)
此時dp[j]
有兩個選擇,一個是取自己dp[j]
相當于 二維dp數組中的dp[i-1][j]
,即不放物品i
,一個是取dp[j - weight[i]] + value[i]
,即放物品i
,指定是取最大的,畢竟是求最大價值,
所以遞歸公式為:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
可以看出相對于二維dp數組的寫法,就是把dp[i][j]
中i
的維度去掉了。
(3)一維dp數組如何初始化
dp[j]
表示:容量為j的背包,所背的物品價值可以最大為dp[j]
,那么dp[0]就應該是0,因為背包容量為0所背的物品的最大價值就是0。dp數組除了下標0的位置,初始為0,其他下標都初始為0就可以了。
(4)一維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]);}
}
這里大家發現和二維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
所以從后往前循環,每次取得狀態不會和之前取得狀態重合,這樣每種物品就只取一次了。
那為什么二維dp數組遍歷的時候不用倒序呢?
因為對于二維dp,dp[i][j]
都是通過上一層即dp[i - 1][j]
計算而來,本層的dp[i][j]
并不會被覆蓋!
再來看看兩個嵌套for循環的順序,代碼中是先遍歷物品嵌套遍歷背包容量,那可不可以先遍歷背包容量嵌套遍歷物品呢?
不可以!
因為一維dp的寫法,背包容量一定是要倒序遍歷(原因上面已經講了),如果遍歷背包容量放在上一層,那么每個dp[j]就只會放入一個物品,即:背包里只放入了一個物品。
所以一維dp數組的背包在遍歷順序上和二維其實是有很大差異的!,這一點大家一定要注意。
(5)舉例推導dp數組
// 一維dp數組實現
#include <iostream>
#include <vector>
using namespace std;int main() {// 讀取 M 和 Nint M, N;cin >> M >> N;vector<int> costs(M);vector<int> values(M);for (int i = 0; i < M; i++) {cin >> costs[i];}for (int j = 0; j < M; j++) {cin >> values[j];}// 創建一個動態規劃數組dp,初始值為0vector<int> dp(N + 1, 0);// 外層循環遍歷每個類型的研究材料for (int i = 0; i < M; ++i) {// 內層循環從 N 空間逐漸減少到當前研究材料所占空間for (int j = N; j >= costs[i]; --j) {// 考慮當前研究材料選擇和不選擇的情況,選擇最大值dp[j] = max(dp[j], dp[j - costs[i]] + values[i]);}}// 輸出dp[N],即在給定 N 行李空間可以攜帶的研究材料最大價值cout << dp[N] << endl;return 0;
}
3. 416分割等和子集
思路分析
(1)問題轉化:
- 要將數組分成兩個和相等的子集,首先需要計算數組的總和。
- 如果數組總和為奇數,那么就不可能分成兩個和相等的子集,因為兩個相等的子集的和必須是偶數。
- 如果數組總和為偶數,我們可以將目標問題轉化為:能否在數組中找到一個子集,其和為總和的一半。
(2)動態規劃:
- 設定目標值為
target = sum(nums) / 2
。 - 然后,我們要判斷是否存在一個子集,其和為
target
。這實際上就是經典的 背包問題。 - 使用動態規劃來判斷能否通過選擇數組中的元素來達到和為
target
。
動規五部曲:
1、確定 dp
數組以及下標的含義
dp[i]
:表示是否存在一個子集,其和為 i
。
2、確定遞推公式
遞推公式基于是否選擇當前元素來更新 dp
數組。
- 不選擇當前元素
dp[j]
仍然保持原值,表示沒有選當前元素的情況。
- 選擇當前元素
- 如果當前背包容量
j
大于等于當前元素nums[i]
,那么可以通過選擇當前元素來更新dp[j]
。具體而言,dp[j] = dp[j] || dp[j - nums[i]]
,即:如果dp[j - nums[i]]
為true
,說明我們可以通過之前的子集加上當前元素nums[i]
,得到背包容量為j
。
- 如果當前背包容量
3、dp
數組如何初始化
-
- 初始時,
dp[0] = true
,表示和為0
是一定可以的(即不選任何元素的情況)。 - 其他所有
dp[j] = false
,表示尚未確定是否能組成j
。
- 初始時,
4、 確定遍歷順序
為了避免重復使用元素,需要倒序遍歷背包容量 j
。(類似上述一維的思路一樣)
- 外層遍歷物品:對于每個元素
nums[i]
,我們都嘗試更新dp
數組。 - 內層遍歷背包容量:背包容量從大到小進行遍歷,確保當前物品
nums[i]
只被使用一次。
遍歷順序:外層遍歷物品(nums[i]
),內層遍歷背包容量 j
(從大到小)。
倒序遍歷背包容量 j
是為了保證每個物品只被使用一次。
5、舉例推導dp數組
class Solution {
public:bool canPartition(vector<int>& nums) {int sum = accumulate(nums.begin(), nums.end(), 0);// 如果總和是奇數,直接返回falseif (sum % 2 != 0) return false;int target = sum / 2;// 初始化dp數組,dp[i]表示是否可以用數組中的一些元素組成和為ivector<bool> dp(target + 1, false);dp[0] = true; // 和為0是可以通過空子集得到的// 遍歷所有的物品for (int num : nums) {// 從后往前遍歷dp數組,防止重復使用同一個元素for (int j = target; j >= num; --j) {dp[j] = dp[j] || dp[j - num];}}// 返回dp[target],表示是否可以湊成和為target的子集return dp[target];}
};