アルゴリズムノート-動的計画法-1(01バックパック)
01バックパック
問題の紹介
標準の01バックパック問題は、n個(int型)のアイテムと最大重量W(int型)のバックパックを指します。重み配列はアイテムの重みを表します。つまり、weight [i]はi番目のアイテムの重みを表します。値配列はアイテムの値を表します。つまり、value [i]はi番目のアイテムの値を表します。アイテムの合計値を最大化するためにバックパックにパックするアイテムを尋ねます。各アイテムは1回しかロードできません。
たとえば、1、バックパックの重量が4で、アイテム情報が次の表で記述されているとします。
重量 | 値 | |
---|---|---|
アイテム0 | 1 | 15 |
アイテム1 | 3 | 20 |
アイテム2 | 4 | 30 |
問題の変換
発生した問題が01バックパックの特性を満たしている場合は、01バックパック問題に変換できるかどうかを検討できます。
- 各アイテム(要素)は1回のみ使用できます
- バックパックがいっぱいです
- 配置されたアイテム(要素)の重みは要素の値であり、値は要素の値でもあります
解決
バックトラック
各アイテムはフェッチされるか、フェッチされないかのいずれかです。バックトラッキング方法を使用して、すべての状況を検索します。時間計算量はO(2 ^ n)です。
動的計画法
動的計画法を使用して、5つのステップで問題を解決します。以下はすべて、たとえば1の2次元dp配列で説明されています。
最初のステップは、dp配列の意味を決定することです
dp配列を図に示します
。dp[i] [j]は、添え字[0〜i]から任意のアイテムを取得し、それを容量jのバックパックに入れることを意味します。値
iの最大合計はアイテムを表します。 、およびjは容量を表します
- iはnよりも小さいことに注意してください。たとえば、n = 3には3つのアイテムがあります。i= 1の場合、0と1の番号のアイテムのみを選択して、バックパックに入れることができます。
- jはバックパックの重量W以下であることに注意してください。バックパックは最大Wを保持できますが、現在のdp配列に対応するバックパックは最大jを保持できます。
2番目のステップは、漸化式を決定することです
dp [i] [j]を導出できるのは2つの方向だけです。
- dp [i-1] [j]によって導入されました。現在のバックパックの容量はjで、最大値は[0〜i-1]の番号のアイテムからのみ選択できます。アイテム番号iのアイテムが追加され、dp [i] [j]はdp [i-1] [ j]
- dp [i-1] [j-weight [i]によって導入されました。dp [i-1] [j-weight [i]]は、バックパックの容量がj-weight [i]の場合のアイテムiの最大値を表します。バックパックはアイテムiの重量を空にするだけなので、アイテムiを追加してみてください。つまり、dp [i] [j] = dp [i-1] [j-weight [i]] + value [i]
したがって、漸化式は次のようになります。
dp [i] [j] = max(dp [i-1] [j]、dp [i-1] [j-重み[i]] +値[i])
初期化の3番目のステップ
dp配列の初期化は、dp配列の定義
に違反することはできません。これは、dp配列の再帰式からわかります。dp配列の導出は、dp配列の前の行の値と前の列の値を使用することです
。したがって、dp配列の最初の行と最初の列が初期化されるとき、バックパックの容量は0になります。完全な初期化は0です。つまり、dp [i] [0] = 0、iは0〜n-1です。
最初の行ではアイテム0のみを選択でき、バックパックの容量は徐々にWに増加します。したがって、dp [0] [j]では、容量jがアイテム0の重み以上の場合、weight [0]、dp [0] [j]はアイテム0の値であり、jがそれより少ない場合は0です。
したがって、最初の行を初期化するときは、再帰式の正の順序のトラバーサルに従います。コードには2つのループがあります。ループを1つだけにしたい場合は、逆のトラバーサルを使用する必要があります。
for (int j = W; j >= weight[0]; j--) {
dp[0][j] = dp[0][j - weight[0]] + value[0];
}
ループトラバーサルが初期化されるとき、コードも繰り返し式に従う必要がありますが、正のシーケンストラバーサルはアイテム0を複数回追加します
for (int j = weight[0]; j <= W; j++) {
dp[0][j] = dp[0][j - weight[0]] + value[0];
}
4番目のステップは、走査順序を決定することです
3番目のステップで初期化されたdp配列は次のとおりです。
トラバーサル次元はアイテムとバックパックウェイトの2つであるため、forループのレイヤーが2つあります。
最初にアイテムをトラバースするか、最初にバックパックウェイトをトラバースしても問題ありませんが、最初にアイテムをトラバースすることをお勧めします。
for(int i = 1; i < n; i++) { // 遍历物品
for(int j = 0; j <= W; 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配列を導出することです。
最終的なdp配列の結果は次のとおりです。
最終的な結果はdp [2] [4]です。
トラバーサル中に判定条件を変更した場合:
if (j - weight[i] >= 0)
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
dp配列
は次のようになります。完全なコードは次のとおりです。
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int W = 4; //背包重量
int n = 3; //物品数量
// 二维数组
vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));
// 初始化
for (int j = W; j >= weight[0]; j--) {
dp[0][j] = dp[0][j - weight[0]] + value[0];
}
// weight数组的大小 就是物品个数
for(int i = 1; i < n; i++) { // 遍历物品
for(int j = 0; j <= W; j++) { // 遍历背包容量
if (j - weight[i] >= 0) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
cout << dp[n][W]<<endl;
}
最適化
1次元のdp配列(ローリング配列)を使用して01ナップサック問題を解く
ことができます。前の行のデータは再利用できることがわかっているため、最初のタイプのdp配列の前の行のデータは直接です。次の行にコピーされる
ため、dp配列を1次元配列に圧縮できます
。Dp:[
0、15、15、20、35 ] dp [j]は、容量がjのバックパックを表します。持ち運ばれるアイテムはdp [j]までです。
漸化式は
dp [j] = max(dp [j]、dp [j --weight [i]] + value [i]);
1次元dp配列のトラバーサル順序は、大から小へのバックパック容量であることに注意してください。各アイテムが1回だけ入れられるようにするために
、トラバーサルシーケンスは最初にアイテムをトラバースしてからトラバースすることしかできません。バックパックの容量
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int W = 4; //背包重量
int n = 3; //物品数量
// 初始化
vector<int> dp(W + 1, 0);
for(int i = 0; i < n; i++) { // 遍历物品
for(int j = W; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[W] << endl;