0-1ナップサック問題
N個のアイテム(番号1、2、..。、N)、
重量int w [N +1]
値intv [N +1]
バックパック容量intS
- 重複するサブ問題がある
- 最適な下部構造を持つ
列挙(+プルーン)
各アイテムが選択され、選択されていないため、バイナリツリーが形成され、次にDFSが形成されます。
動的計画法
再帰的書き込み:
dp(i、x)は、項目1〜iから選択することを意味し、バックパックの残りの容量はxであり、この状態で到達できる最大値です。
- 漸化式
dp(i、x)= max {dp(i-1、x)、dp(i-1、xw [i])+ v [i]} - 再帰的境界
i = 0またはx = 0、dp = 0
x <0、dp =-∞の場合、この状況は不可能であることを意味し、最大で排除されます。
#include <climits>
#include <algorithm>
const int NINF = INT_MIN;
int DP(int i, int x){
if(i==0 || x==0) return 0;
if(x<0) return NINF;
return std::max(DP(i-1,x), DP(i-1, x-w[i])+v[i]);
}
int main(){
int ans = DP(N, S);
}
再帰のサブ問題の計算が繰り返されます。実際の計算量は、再帰が「残り容量」で後ろから前に列挙され、後者が「累積重量」で前から後ろに列挙されることを除いて、列挙+剪定と同じです。
再帰的な書き込み:
- 状態
dp [i] [x]は、バックパックの残りの容量が項目1からiからx選択されたときに、この状態で到達できる最大値を表します。 - 必要态転移方程
dp [i] [x] = {max {dp [i − 1] [x]、dp [i − 1] [x − w [i]] + v [i]}ifx≥w[i ] dp [i − 1] [x] if x <w [i] dp [i] [x] = \ begin {cases} max \ lbrace dp [i-1] [x]、dp [i-1] [ xw [i]] + v [i] \ rbrace&\ text {if} x \ geq w [i] \\ dp [i-1] [x]&\ text {if} x <w [i] \ end {ケース}d p [ i ] [ x ]={{ m a x { d p [ i−1 ] [ x ] 、d p [ i−1 ] [ x−w [ i ] ]+v [ i ] }d p [ i−1 ] [ X ]xの場合 ≥w [ i ]xの場合 <W [ I ] - 边界
dp [0] [x] = 0、(0 <= x <= S)
dp [i] [0] = 0、(1 <= i <= N) - 処理順序
i \ x | 0 | 1 | 2 | 3 | … | S |
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | … | 0 |
1 | 0 | |||||
2 | 0 | |||||
3 | 0 | |||||
… | … | |||||
N | 0 |
状態遷移方程式に含まれる状態はすべて(i、x)ポイントの上または上にあるため、処理シーケンスは左から右、上から下になります。
#include <algorithm>
int dp[N+1][S+1]={
}; //边界
for(int i=1; i<=N; i++){
for(int x=1; x<=S; x++){
if(x>=w[i]) dp[i][x] = std::max(dp[i-1][x], dp[i-1][x-w[i]]+v[i]);
else dp[i][x] = dp[i-1][x];
}
}
int ans = dp[N][S];
最終的な回答点(N、S)は右下隅にあり、関係する可能性のある点は左上と上にパスを形成しますが、計算前にパスが何であるかを知ることは不可能であるため、大きい最終的な答えに役に立たないポイントの数が計算される場合があります。
動的計画法は依然としてすべての状況の列挙を回避することはできません。その効率改善の本質は、「ストレージ状態+状態の後遺症なし+状態再発式」によって状態の計算を高速化し、繰り返し計算する必要をなくすことです。
- 時間計算量の最適化:O(NS)が限界であり、最大で(N、0)〜(N、S-1)を節約します
- 空間の複雑さの最適化:
特定の中間点まで計算する場合、使用できる点は青い枠で囲まれた部分のみです。したがって、2次元配列は1次元配列に縮小でき、xは大きいからです。 to smallおよびifrom small toポイント(N、S)まで配列を大きな順序で更新します。
int dp[S+1]={
};
for(int i=1; i<=N; i++){
for(int x=S; x>=1; x--){
if(x>=w[i]) dp[x] = std::max(dp[x], dp[x-w[i]]+v[i]);
//else dp[x]=dp[x]
}
}
//进一步优化写法
for(int i=1; i<=N; i++){
for(int x=S; x>=w[i]; x--){
dp[x] = std::max(dp[x], dp[x-w[i]]+v[i]);
}
}
完全なナップサック問題
各アイテムの数量を無限に変更します。
動的計画法
再帰
上記のステータス- 必要态転移方程
dp [i] [x] = {max {dp [i − 1] [x]、dp [i] [x − w [i]] + v [i]}ifx≥w[i] dp [i − 1] [x] if x <w [i] dp [i] [x] = \ begin {cases} max \ lbrace dp [i-1] [x]、dp [i] [xw [i] ] + v [i] \ rbrace&\ text {if} x \ geq w [i] \\ dp [i-1] [x]&\ text {if} x <w [i] \ end {cases}d p [ i ] [ x ]={{ m a x { d p [ i−1 ] [ x ] 、d p [ i ] [ x−w [ i ] ]+v [ i ] }d p [ i−1 ] [ X ]xの場合 ≥w [ i ]xの場合 <W [ I ] - 境界
上記と同じ - 処理シーケンス
新しい状態遷移方程式に含まれるポイントは、現在のポイントの左側と上にあります。したがって、処理シーケンスは変更されません。
スペースの最適化後、iとxは小さいものから大きいものの順に処理されます。
int dp[S+1]={
};
for(int i=1; i<=N; i++){
for(int x=1; x<=S; x++){
if(x>=w[i]) dp[x] = std::max(dp[x], dp[x-w[i]]+v[i]);
//else dp[x]=dp[x]
}
}
//进一步优化写法
for(int i=1; i<=N; i++){
for(int x=w[i]; x<=S; x++){
dp[x] = std::max(dp[x], dp[x-w[i]]+v[i]);
}
}