変更2は、典型的なナップザック問題のもう1つの変形です。前の記事で、古典的な動的プログラミングについてすでに説明しました :0-1ナップザック問題 と ナップザック問題の変形:等しいサブセット分割。
最初の2つの記事を読み、動的プログラミングとナップザック問題ルーチンを読んだことを願っています。この記事では、引き続きナップザック問題ルーチンに従い、ナップザック問題の変形をリストします。
この記事は、LeetCodeの質問518、Coin Change 2に関するもので、タイトルは次のとおりです。
int change(int amount、int [] coins);
PS:Coin Change 1 については、前回の記事で動的計画ルーチンについて説明しました 。
この問題をナップザック問題の記述形式に変えることができます:
バックパック、最大容量amount
、一連の記事coins
、各項目の重みcoins[i]
、各アイテムの無制限。バックパックを正確に満たす方法はいくつありますか?
この問題と先ほどお話しした2つのナップザックの問題の最大の違いは、各アイテムの数が無限であるということです。これは伝説的な「完全なナップザックの問題」です。背の高いものはなく、ステータスにすぎません。転送式にわずかな変更があります。
以下は、バックパックの問題の形式を説明しています。プロセスに従って分析を続けます。
問題解決のアイデア
最初のステップは、「ステータス」と「選択」の2つのポイントを明確にすることです。
「バックパックの容量」と「選択可能なアイテム」の2つの状態があり、「バックパックにパックする」または「バックパックにない」のどちらかを選択します。バックパックの問題のルーチンは同じです。
ステータスと選択を理解すると、このフレームワークを適用する限り、動的プログラミングの問題は基本的に解決されます。
状態1のすべての値の状態1の場合: 状態2のすべての値の状態2の 場合... dp [state1] [state2] [...] =計算(1を選択し、2を選択します。 。)
2番目のステップはdp
、配列の定義を明確にすることです。
最初に見つけた「ステータス」を見てください。2つあります。つまり、2次元dp
配列が必要です。
dp[i][j]
定義は次のとおりです。
前のi
2つのアイテムだけの場合、バックパックを埋める方法j
があるときのバックパックの容量dp[i][j]
。
言い換えれば、私たちのタイトルに戻る翻訳は次のことを意味します:
唯一の場合はcoins
、フロントi
あなたはCouchu量をしたい場合は、コインの額面、j
があるdp[i][j]
エラー方法の種類が。
上記の定義の後、次のようになります。
基本ケースはdp[0][..] = 0, dp[..][0] = 1
です。なぜなら、コインの額面金額を使用しないと、金額を補うことができないからです。目標金額が0の場合、「何もしないことによるルール」が補う唯一の方法です。
最終的に取得したい答えはdp[N][amount]
、です。ここN
でcoins
、は配列のサイズです。
一般的な疑似コードの考え方は次のとおりです。
int dp [N + 1] [amount + 1] dp [0] [..] = 0 dp [..] [0] = 1 for i in [1..N]: for j in [1..amount ]: アイテムiをバックパックに 入れるのではなく、アイテムiをバックパックに入れる return dp [N] [amount]
3番目のステップは、「選択」に基づいて状態遷移のロジックについて考えることです。
私たちの問題の特別な点は、アイテムの数が無制限であるということです。したがって、これは以前に書かれたバックパックの問題の記事とは異なります。
この最初のi
アイテムをバックパックにパックしない場合、つまりcoins[i]
この金種のコインを使用しない場合、金種j
を構成する方法の数は前の結果dp[i][j]
と同じになるはずdp[i-1][j]
です。
この最初のi
アイテムをバックパックに入れる場合、つまりcoins[i]
この金種のコインを使用する場合、それdp[i][j]
はに等しいはずdp[i][j-coins[i-1]]
です。
以来、最初は、i
それは1から始まり、coins
インデックスがあるの金種最初の硬貨。i-1
i
dp[i][j-coins[i-1]]
このコインを使用する場合は、金額の徴収方法に注意する必要があることを理解するのは難しいことではありませんj - coins[i-1]
。
たとえば、額面金額が2のコインを使用して金額を5にする場合、金額を3にする方法を知っていて、額面金額が2のコインを追加する場合は、5を補うことができます。
PS:私は100以上のオリジナル記事を注意深く書き、200のバックルの質問を手作業でブラッシングしました。これらはすべて、labuladongのアルゴリズムチートシートに公開されており、継続的に更新されています。収集し、私の記事の順序で質問をブラッシングし、さまざまなアルゴリズムルーチンを習得し、それらを質問の海にキャストすることをお勧めします。
要約すると、2つの選択肢があり、私たちが見つけたいのdp[i][j]
は「一緒にそれを行う方法の数」であるためdp[i][j]
、値は上記の2つの選択肢の結果の合計である必要があります。
for(int i = 1; i <= n; i ++){ for(int j = 1; j <= amount; j ++){ if(j --coins [i-1]> = 0) dp [i] [j ] = dp [i-1] [j] + dp [i] [j-coins [i-1]]; dp [N] [W]を返します
最後のステップは、疑似コードをコードに変換し、いくつかの境界条件を処理することです。
私はJavaでコードを書き、上記のアイデアを完全に翻訳し、いくつかの境界の問題に対処しました。
int change(int amount、int [] coins){ int n = coins.length; int [] [] dp =量int [n + 1] [amount + 1]; // (int i = 0; i <= n; i ++)の 基本ケース dp [i] [0] = 1; for(int i = 1; i <= n; i ++){ for(int j = 1; j <= amount; j ++) if(j --coins [i-1]> = 0) dp [i] [j] = dp [i-1] [j] + dp [i] [j-コイン[i-1]]; それ以外の場合、 dp [i] [j] = dp [i-1] [j]; } return dp [n] [amount]; }
さらに、観察を通じて発見しdp
、配列を転送するだけdp[i][..]
でdp[i-1][..]
あるため、圧縮状態にすることができ、アルゴリズムのスペースの複雑さをさらに軽減します。
int change(int amount、int [] coins){ int n = coins.length; int [] dp = new int [amount + 1]; dp [0] = 1; // (int i = 0; i <n; i ++) for(int j = 1; j <= amount; j ++) if(j --coins [i]> = 0) dp [j] = dp [の基本ケースj] + dp [j-coins [i]]; dp [amount]を返します。 }
このソリューションは前のアイデアとまったく同じでdp
、2次元配列を1次元に圧縮し、時間の複雑さはO(N * amount)、空間の複雑さはO(amount)です。
これまでのところ、この変更交換の問題は、バックパック問題のフレームワークによっても解決されています。