さまざまな額面のコインを表す整数の配列 coins
と、 amount
合計金額を表す別の整数が与えられます。合計金額を構成できるコインの組み合わせの数を計算して返してください。合計金額に達しないコインの組み合わせがある場合は、 を返します 0
。
各金種のコインが無限にあると仮定します。質問データは、結果が 32 ビット符号付き整数に準拠していることを保証します。
例 1:
入力:金額 = 5、コイン = [1, 2, 5] 出力: 4 説明:合計金額を計算するには 4 つの方法があります: 5=5 5=2+2+1 5=2+1+1+1 5 =1+1+1+1+1
例 2:
入力:金額 = 3、コイン = [2] 出力: 0 説明:合計金額 3 は、金種 2 のコインだけを使用して構成することはできません。
例 3:
入力:金額 = 10、コイン = [10] 出力: 1
>>アイデアと分析
- ① コインの数に制限がないため、これは完全なバックパックの問題であることがわかります。
- ②純粋なバックパックと組み合わせられるバックパックの最大値はいくらですか? 合計金額を構成するために必要なアイテムの組み合わせ数に関する質問です!
なお、質問文では総額を構成するコインの組み合わせの数について言及していますが、なぜ組み合わせの数であることが強調されているのでしょうか。
たとえば、例 1:
5 = 2 + 2 + 1
5 = 2 + 1 + 2
これは両方 2 2 1 の組み合わせです。順列の数について尋ねる場合、上記の 2 つの順列があります。
注:組み合わせでは要素間の順序が強調されませんが、順列では要素間の順序が強調されます。
>>動的ルールの 5 つのステップ
1. dp 配列と添字の意味を確認する
dp[j]: 合計金額 j を構成する通貨の組み合わせの数は dp[j]
2. 漸化式を決定する
- dp[j] は、すべてのdp[j - コイン[i]] の合計です(コイン[i] の場合を考慮)
- したがって、再帰式は次のようになります。dp[j] += dp[j - Coins[i]];
0-1 バックパックの質問はLeetCode 494 で説明されています。 Goal He . バックパックを埋めるにはいくつかの方法があり、公式は次のとおりです。
dp[j] += dp[j - nums[i]];
3.dp配列の初期化
dp[0] = 1、これは再帰式の基礎です。dp[0] = 0 の場合、後続のすべての派生値は 0 になります。
0 以外の添え字を持つ dp[j] は 0 に初期化されるため、dp[j - コイン[i]] の累積加算時に実際の dp[j] は影響を受けません。
dp[0] = 1 も状況を示しています。coins[i] が選択された場合、つまり j - Coins[i] == 0 はこのコインを選択できることを意味し、dp[0] が 1 はそのようなコインが存在することを意味します。コインのみを選択する選択方法[i]
4. 走査順序を決定する
- 方法 1: 最初にアイテムを移動し、次にバックパックを移動します。
- 方法 2: 最初にバックパックを横断し、次にアイテムを横断する
純粋に完全なバックパックでは、最初にアイテムをトラバースしてからバックパックをトラバースするか、最初にバックパックをトラバースしてからアイテムをトラバースするかは関係ありません。
この質問は役に立ちません! 純粋に完全なナップザックが完全なナップザックの最大値を決定するため、合計を構成する要素が順序どおりであるかどうかとは何の関係もありません。つまり、順序どおりであるかどうかは問題ではありません。
ただし、この問題では、和を構成するために必要な組み合わせの数に関しては、要素間に順序がないという明確な要件があります。つまり、方法 1 の走査順序のみを指定できるのに、なぜ方法 2 を使用できないのでしょうか?
完全なバックパックの方法 1 の場合: 外側の for ループはアイテム (コイン) を走査し、内側の for ループはバックパック (合計金額) を走査します。
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
dp[j] += dp[j - coins[i]];
}
}
仮定: コイン[0] = 1、コイン[1] = 5
次に、最初に 1 を計算に加え、次に 5 を計算に加えます。得られるのは状況 {1,5} だけであり、状況 {5,1} は発生しないため、この走査シーケンスでは dp[j]ここで計算されるのは組み合わせの数です!
完全なバックパックの 2 番目のメソッドの場合: 外側の for ループはバックパック (合計金額) を横断し、内側の for ループはアイテム (コイン) をループします。
for (int j = 0; j <= amount; j++) { // 遍历背包容量
for (int i = 0; i < coins.size(); i++) { // 遍历物品
if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
}
}
バックパックの容量の各値は、{1,5} と {5,1} を含め、1 と 5 で計算されます。このとき、dp[j]で計算されるのは順列数です!
5. dp 配列の導出と例
入力: 金額 = 5、コイン = [1,2,5]、DP 状態図は次のとおりです。
dp[amout] は最終結果です
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1, 0);
dp[0] = 1;
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
};
- 時間計算量: O(mn)、m はコインの量、n はコインの長さ
- 空間の複雑さ: O(m)
【まとめ】
この問題の漸化式は 494. Target Sum で詳しく説明されていますが、この問題の難しさは主に探索順序にあります。
- バックパックに荷物を詰めるオプションがいくつかある場合、どの順序で荷物を運ぶかを決定することが非常に重要です。
- 組み合わせの数を調べたい場合は、外側の for ループで項目を走査し、内側の for ループでバックパックを走査します。
- 順列の数を調べたい場合は、外側の for ループはバックパックを走査し、内側の for ループは項目を走査します。
Code Caprice のクラスのスクリーンショット:
参考記事と動画:
コードランダムレコード (programmercarl.com)
動的プログラミングの完全なナップザック。ナップザックを埋める方法は何通りありますか? 組み合わせや配置にも注目!| LeetCode: 518. 変更交換 II_bilibili_bilibili