1. バックパック問題の概要:
2. 暴力的な解決策:
重さ | 価値 | |
---|---|---|
アイテム0 | 1 | 15 |
項目 1 | 3 | 20 |
項目 2 | 4 | 30 |
バックパックの最大収容人数は4人です。
各項目には「取る」または「取らない」の 2 つの状態があります。バックトラッキング法を使用すると、すべてのアイテムのステータスの順列と組み合わせを激しく列挙し、それをバックパックの最大容量と比較して最大値を見つけることができます。時間計算量は O ( 2 n ) O(2^n) です。 )○ (2n )は指数レベルにあるため、最適化には動的計画法ソリューションが必要です。
3.二次元DPアレイソリューション01バックパック
1. DP配列の意味
dp[i][j]
[0,i]
:の番号のアイテムを 、容量 のバックパックに入れることでj
得られる最大値。
2. 漸化式(ペアdp[i][j]
)
- 項目はありません
i
:dp[i][j]=dp[i-1][j]
- アイテムを置く
i
:dp[i][j]=dp[i-1][j-weight[i]] + value[i]
dp[i][j]
最終的には、アイテムを置くi
か置かないかi
の大きい方の価値をとるべきです。
したがって、dp[i][j]=max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])
3.DPアレイ解析
図に示すように、dp[i][j]
(赤い表) については、その値は 2 つの方向から取得されます。
dp[i][j]=dp[i-1][j]
、赤い矢印1番から得られます。dp[i][j]=dp[i-1][j-weight[i]] + value[i]
、矢印2番で得られます。矢印 2 の具体的な初期位置はweight[i]
次のように決定されます。
したがって、 DP 配列を解くときに[i-1]
それを知る必要があるため、DP 配列を初期化するときに最初の行を初期化する必要があります。
最初の列を初期化する必要はなく、if
判定を使用するだけですj-weight[i] > 0
。
要約すると、初期化中に最初の行のみが初期化され、残りの位置を初期化する必要はありません。
この質問の場合、初期化配列は次のとおりです。
4. 走査順序
最初にアイテムを調べてからバックパックを調べます
この方法の本質は、行を走査し、各項目を容量から容量0
までj
1 つずつテストすることです。これは DP 配列を解析することで取得できます。検索dp[i][j]
時には、その中のすべてのデータが既知である必要がありdp[i-1][0~j]
、これは前のループで取得されています。したがって、このトラバース方法は実行可能です。
ii 最初にバックパックを横断し、次にアイテムを横断します
この方法の本質は、列を横断してアイテムからアイテム0
までi
各容量を 1 つずつテストすることです。これは DP 配列を解析することで取得できます。検索dp[i][j]
時には、その中のすべてのデータが既知である必要がありdp[i-1][0~j]
、これは前のループで取得されています。したがって、このトラバース方法は実行可能です。
5. コード:
void knapsack () {
vector<int> weight = {
1, 3, 4}; // 物品重量
vector<int> value = {
15, 20, 30}; // 物品价值
int bagweight = 4; // 背包的最大容量
// 创建二维数组
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 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];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
// 先遍历背包
// for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
// for(int i = 1; i < weight.size(); i++) { // 遍历物品
// 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]);
// }
// }
cout << dp[weight.size() - 1][bagweight] << endl;
}
4. 01 バックパックへの 1 次元 DP アレイ (ローリング アレイ) ソリューション
1. DP配列の意味
dp[i][j]
2 次元 DP 配列のトラバーサル図から、このソリューションでは DP 配列の前の行 (または列) のデータが完全に使用されており、dp[i][j]
後続の内容はまったく考慮されていないことがわかります。したがって、前行(または前列)のデータを現在の行に上書きすることを考えると、1行(または1列)で計算を完了できる、というのが1次元DP配列の考え方です。この質問では。
dp[j]
この質問は、(2 次元配列を 1 行に圧縮するのと同等) の容量のバックパックにj
入れることができるアイテムの最大値として定義されます。
2. 漸化式
- 項目を置かないでください
i
:dp[j]=dp[j]
(dp[j]
等号以降のデータは前の行のデータとみなすことができますが、現在の行が上書きされるだけです) - 項目の配置
i
: (等号以降は前の行のデータとdp[j]=dp[j-weight[i]]+value[i]
考えることができますが、現在の行を上書きするだけです)dp[j-weight[i]]
dp[j]=max(dp[j], dp[j-weight[i]]+value[i])
3.DP配列の初期化
配列の定義から、配列dp[j]
の前にあるデータのみを知る必要があることがわかります。考えてみてください。「表」のデータは、バックパックには何も入っていないということです。したがって、DP 配列のすべての要素を 0 に設定するだけです。
4. 走査順序
現在の DP 配列の値は前の行のオーバーレイとして見えるため、dp[j]
前の要素を「クリーン」に保つために、j
トラバーサル中にフラッシュバック トラバーサルを使用する必要があります。
図に示すように、青は現在の行の更新された値を表し、赤は現在の行によって要求されている値を表し、緑はまだ更新されていない前の行の値を表します。後ろから前に移動するとdp[j]
、更新時に前の値が変更されなくなります。dp[j]
前方トラバーサルが使用される場合、前の値が現在の行の値であることと等価になります(このステートメントも正しくありません。i
項目の値は累積されます)。再帰式は成立しません。
バックパックのアイテムの重量{1, 1}
、値、および最大容量が 4 であると仮定します。{5, 10}
図のように、
2行目のDPを順方向トラバーサルで更新すると、dp[j]
前のデータが汚染されているため、dp[j]
更新されるたびに項目1の値が累積されます。フラッシュバック中は、以前のデータが削除されていないため污染
、累積エラーは発生しません。図に示すように:
5.コード
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;
}