From 050d68dac75edd5cd76c142584a533cd45b3fe6d Mon Sep 17 00:00:00 2001 From: seal_azarashi Date: Wed, 6 Nov 2024 08:06:12 +0900 Subject: [PATCH 1/8] step 1 & 2 --- arai60/Dynamic_Programming/coin-change.md | 243 ++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 arai60/Dynamic_Programming/coin-change.md diff --git a/arai60/Dynamic_Programming/coin-change.md b/arai60/Dynamic_Programming/coin-change.md new file mode 100644 index 0000000..65b1ae8 --- /dev/null +++ b/arai60/Dynamic_Programming/coin-change.md @@ -0,0 +1,243 @@ +# 322. Coin Change + +LeetCode URL: https://leetcode.com/problems/coin-change/description/ + +この問題は Java で解いています。 +各解法において、メソッドが属するクラスとして `Solution` を定義していますが、これは Java の言語仕様に従い、コードを実行可能にするために必要なものです。このクラス自体には特定の意味はなく、単にメソッドを組織化し、実行可能にするためのものです。 + +## Step 1 + +まずは愚直にあらゆる組み合わせ (coin の合算値が amount を超えるものは除く) を出す方法を考えつくが、計算量の面から好ましくないので別のアプローチが無いか探したい気持ちになる。 +すぐに次のアプローチが思いついたので、以下の通り実装した: 各インデックスの数字と同じ数値の額を何枚のコインで作れるかを保持する配列を作り、各要素を更新する。1 から amount までの各整数値ごとに coins の要素数だけ計算が必要なので計算量は O(n * m) で悪くないはず。 + +```java +/** + * 時間計算量: O(n * m): + * - amount + 1 回処理を実行: O(n) + * - coins の要素数だけ処理を実行: O(m) + * - 条件判定といった定数計算量の操作: O(1) + * 空間計算量: O(n): + * - キャッシュ用配列: O(n) + * - その他定数計算量の変数等: O(1) + * + * ※ n = amount, m = coins.length + */ +class Solution { + private final int CANNOT_BE_MADE_UP = -1; + + public int coinChange(int[] coins, int amount) { + /** + * 各インデックスの数字と同じ数値の額を何枚のコインで作れるかを保持する。 + * 計算の都合上、額が0の際は必要な枚数も0であるという情報が欲しいので、そのために要素数を + 1 している。 + */ + int[] minCoinCountToMakeUp = new int[amount + 1]; + + for (int i = 1; i < minCoinCountToMakeUp.length; i++) { + int minCount = Integer.MAX_VALUE; + boolean isAnyCountFound = false; + for (int coin : coins) { + int remainder = i - coin; + if (remainder < 0 || minCoinCountToMakeUp[remainder] == CANNOT_BE_MADE_UP) { + continue; + } + + minCount = Math.min(minCount, minCoinCountToMakeUp[remainder]); + isAnyCountFound = true; + } + if (!isAnyCountFound) { + minCoinCountToMakeUp[i] = CANNOT_BE_MADE_UP; + continue; + } + + minCoinCountToMakeUp[i] = minCount + 1; + } + + return minCoinCountToMakeUp[amount]; + } +} +``` + +## Step 2 + +### 1 から amount まで、それぞれ何枚のコインで作れるかをキャッシュする (Step 1 と同じ) + +- ちょっとパズル感がある (下の方まで処理を読まないと各要素が何を表すのか想像しづらい) 印象があったので、キャッシュ用配列の宣言時にいくつか処理を寄せ、コードコメントを追加した。コードコメントの一部は javadoc に書いてもいいかもしれない。 +- 変数名もいくつかより理解しやすそうなものに修正している。 +- Leetcode 上でなく実務での利用を想定して null チェックも入れた。 +- minCoinCount の値は `amount + 1` でも良いというコメントがあったが、その数値が何なのかを読み手が理解しなければならないことに抵抗があったので採用を見送った。既存の Integer.MAX_VALUE も全く理解が必要ないわけでは無いが、より素直に思える。 + +```java +/** + * 時間計算量: O(n * m): + * - キャッシュ用配列の作成: O(n) + * - amount + 1 回処理を実行: O(n) + * - coins の要素数だけ処理を実行: O(m) + * - 条件判定といった定数計算量の操作: O(1) + * 空間計算量: O(n): + * - キャッシュ用配列: O(n) + * - その他定数計算量の変数等: O(1) + * + * ※ n = amount, m = coins.length + */ +class Solution { + private final int NO_COMBINATION = -1; + + public int coinChange(int[] coins, int amount) { + Objects.requireNonNull(coins, "Argument coins must not be null"); + + /** + * 各インデックスの数字と同じ数値の額を何枚のコインで作れるかを保持する。 + * 計算の都合上、額が0の際は必要な枚数も0であるという情報が欲しいので、そのために要素数を + 1 している。 + * インデックス 0 の初期値は 0 で、他は計算の都合上、その額が作れないことを示す値を入れておく。作れることが判明次第更新される。 + */ + int[] makeUpAmountToCoinCount = new int[amount + 1]; + Arrays.fill(makeUpAmountToCoinCount, NO_COMBINATION); + makeUpAmountToCoinCount[0] = 0; + + for (int currentAmount = 1; currentAmount < makeUpAmountToCoinCount.length; currentAmount++) { + int minCoinCount = Integer.MAX_VALUE; + boolean isAnyCombinationFound = false; + for (int coin : coins) { + int remainder = currentAmount - coin; + if (remainder < 0 || makeUpAmountToCoinCount[remainder] == NO_COMBINATION) { + continue; + } + + int coinCountToMakeUpWithTheCoin = makeUpAmountToCoinCount[remainder] + 1; + minCoinCount = Math.min(minCoinCount, coinCountToMakeUpWithTheCoin); + isAnyCombinationFound = true; + } + if (isAnyCombinationFound) { + makeUpAmountToCoinCount[currentAmount] = minCoinCount; + } + } + return makeUpAmountToCoinCount[amount]; + } +} +``` + +### 非再帰の DFS + +他の方の解法にあったので書いてみる。 +実行時間が上の解法より10倍以上長いのが気になる。Record オブジェクトの作成やスタックの操作にメモリのランダムアクセスが行われるからだろうか。まだ改善は出来そうだが一旦ここまで。 + +```java +/** +* 時間計算量: O(n * m): +* - 入力コインの前処理: O(m) +* - キャッシュ用配列の作成: O(n) +* - スタックを使用した探索: O(n * m) +* - 最悪の場合、各金額(n)に対して全てのコイン(m)を試す +* - ただし枝刈りにより実際の計算回数は減少する可能性あり +* 空間計算量: O(n * m): +* - キャッシュ用配列: O(n) +* - スタック: 最悪の場合 O(n * m) +* - その他定数計算量の変数等: O(1) +* +* ※ n = amount, m = coins.length +*/ +class Solution { + private final int NO_COMBINATION = -1; + + private record CoinState(int numCoins, int currentAmount) {} + + public int coinChange(int[] coins, int amount) { + Objects.requireNonNull(coins, "Argument coins must not be null"); + + int[] filteredCoins = Arrays.stream(coins) + .filter(coin -> coin <= amount) + .toArray(); + + int[] makeUpAmountToCoinCount = new int[amount + 1]; + Arrays.fill(makeUpAmountToCoinCount, Integer.MAX_VALUE); + + Stack coinStateStack = new Stack<>(); + coinStateStack.push(new CoinState(0, 0)); + + while (!coinStateStack.isEmpty()) { + CoinState coinState = coinStateStack.pop(); + makeUpAmountToCoinCount[coinState.currentAmount()] = Math.min( + makeUpAmountToCoinCount[coinState.currentAmount()], + coinState.numCoins() + ); + for (int coin : filteredCoins) { + int nextAmount = coinState.currentAmount() + coin; + if (nextAmount > amount) { + continue; + } + if (makeUpAmountToCoinCount[nextAmount] <= coinState.numCoins() + 1) { + continue; + } + coinStateStack.push(new CoinState(coinState.numCoins() + 1, nextAmount)); + } + } + + return makeUpAmountToCoinCount[amount] == Integer.MAX_VALUE + ? NO_COMBINATION + : makeUpAmountToCoinCount[amount]; + } +} +``` + +### 再帰の DFS + +スタックオーバーフローのリスクがあるので優先して選ぶことはないですが、再帰による実装も書いてみます。 + +```java +/** +* 時間計算量: O(n * m): +* - 入力コインの前処理: O(m) +* - キャッシュ用配列の作成: O(n) +* - 再帰的な探索: O(n * m) +* - 最悪の場合、各金額(n)に対して全てのコイン(m)を試す +* - ただし枝刈りにより実際の計算回数は減少する可能性あり +* 空間計算量: O(n): +* - キャッシュ用配列: O(n) +* - 再帰スタック: 最悪の場合 O(n) +* - その他定数計算量の変数等: O(1) +* +* ※ n = amount, m = coins.length +*/ +class Solution { + private final int NO_COMBINATION = -1; + + public int coinChange(int[] coins, int amount) { + Objects.requireNonNull(coins, "Argument coins must not be null"); + + int[] filteredCoins = Arrays.stream(coins) + .filter(coin -> coin <= amount) + .toArray(); + + int[] makeUpAmountToCoinCount = new int[amount + 1]; + Arrays.fill(makeUpAmountToCoinCount, Integer.MAX_VALUE); + makeUpAmountToCoinCount[0] = 0; + + updateMinCoinCountsRecursively(filteredCoins, amount, 0, 0, makeUpAmountToCoinCount); + + return makeUpAmountToCoinCount[amount] == Integer.MAX_VALUE + ? NO_COMBINATION + : makeUpAmountToCoinCount[amount]; + } + + private void updateMinCoinCountsRecursively( + int[] coins, + int targetAmount, + int currentAmount, + int numCoins, + int[] makeUpAmountToCoinCount + ) { + makeUpAmountToCoinCount[currentAmount] = Math.min(makeUpAmountToCoinCount[currentAmount], numCoins); + + for (int coin : coins) { + int nextAmount = currentAmount + coin; + if (nextAmount > targetAmount) { + continue; + } + if (makeUpAmountToCoinCount[nextAmount] <= numCoins + 1) { + continue; + } + updateMinCoinCountsRecursively(coins, targetAmount, nextAmount, numCoins + 1, makeUpAmountToCoinCount); + } + } +} +``` From 26235292be0073b0922fe6b528a8c9efb34e30f2 Mon Sep 17 00:00:00 2001 From: seal_azarashi Date: Wed, 6 Nov 2024 08:16:49 +0900 Subject: [PATCH 2/8] add step 2 --- arai60/Dynamic_Programming/coin-change.md | 74 ++++++++++++++++++++--- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/arai60/Dynamic_Programming/coin-change.md b/arai60/Dynamic_Programming/coin-change.md index 65b1ae8..0c325c9 100644 --- a/arai60/Dynamic_Programming/coin-change.md +++ b/arai60/Dynamic_Programming/coin-change.md @@ -59,7 +59,7 @@ class Solution { ## Step 2 -### 1 から amount まで、それぞれ何枚のコインで作れるかをキャッシュする (Step 1 と同じ) +### 1 から amount まで、それぞれ何枚のコインで作れるかをキャッシュする DP (Step 1 と同じ) - ちょっとパズル感がある (下の方まで処理を読まないと各要素が何を表すのか想像しづらい) 印象があったので、キャッシュ用配列の宣言時にいくつか処理を寄せ、コードコメントを追加した。コードコメントの一部は javadoc に書いてもいいかもしれない。 - 変数名もいくつかより理解しやすそうなものに修正している。 @@ -116,10 +116,72 @@ class Solution { } ``` +### BFS + +他の方の解法にあったので書いてみる。TLE。 +DP の解法より遅い理由は、record オブジェクトの作成やスタックの操作にメモリのランダムアクセスが行われるからだろうか。まだ改善は出来そうだが一旦ここまで。 + +```java +/** +* 時間計算量: O(n * m): +* - 入力コインの前処理: O(m) +* - キャッシュ用配列の作成: O(n) +* - キューを使用した探索: O(n * m) +* - 最悪の場合、各 amount に対して全てのコインを試す +* 空間計算量: O(n * m): +* - キャッシュ用配列: O(n) +* - キュー: 最悪の場合 O(n * m) +* - その他定数計算量の変数等: O(1) +* +* ※ n = amount, m = coins.length +*/ +class Solution { + private final int NO_COMBINATION = -1; + + private record CoinState(int numCoins, int currentAmount) {} + + public int coinChange(int[] coins, int amount) { + Objects.requireNonNull(coins, "Argument coins must not be null"); + + int[] filteredCoins = Arrays.stream(coins) + .filter(coin -> coin <= amount) + .toArray(); + + int[] makeUpAmountToCoinCount = new int[amount + 1]; + Arrays.fill(makeUpAmountToCoinCount, Integer.MAX_VALUE); + + Queue coinStateQueue = new ArrayDeque<>(); + coinStateQueue.offer(new CoinState(0, 0)); + + while (!coinStateQueue.isEmpty()) { + CoinState coinState = coinStateQueue.poll(); + makeUpAmountToCoinCount[coinState.currentAmount()] = Math.min( + makeUpAmountToCoinCount[coinState.currentAmount()], + coinState.numCoins() + ); + + for (int coin : filteredCoins) { + int nextAmount = coinState.currentAmount() + coin; + if (nextAmount > amount) { + continue; + } + if (makeUpAmountToCoinCount[nextAmount] <= coinState.numCoins() + 1) { + continue; + } + coinStateQueue.offer(new CoinState(coinState.numCoins() + 1, nextAmount)); + } + } + + return makeUpAmountToCoinCount[amount] == Integer.MAX_VALUE + ? NO_COMBINATION + : makeUpAmountToCoinCount[amount]; + } +} +``` + ### 非再帰の DFS -他の方の解法にあったので書いてみる。 -実行時間が上の解法より10倍以上長いのが気になる。Record オブジェクトの作成やスタックの操作にメモリのランダムアクセスが行われるからだろうか。まだ改善は出来そうだが一旦ここまで。 +他の方の解法にあったので書いてみる。実行時間が上の解法より10倍以上長いが TLE にはならなかった。BFS の方が効率は良さそうに思えたが、テストケースとの相性がたまたまよかったのだろうか。 ```java /** @@ -127,8 +189,7 @@ class Solution { * - 入力コインの前処理: O(m) * - キャッシュ用配列の作成: O(n) * - スタックを使用した探索: O(n * m) -* - 最悪の場合、各金額(n)に対して全てのコイン(m)を試す -* - ただし枝刈りにより実際の計算回数は減少する可能性あり +* - 最悪の場合、各 amount に対して全てのコインを試す * 空間計算量: O(n * m): * - キャッシュ用配列: O(n) * - スタック: 最悪の場合 O(n * m) @@ -189,8 +250,7 @@ class Solution { * - 入力コインの前処理: O(m) * - キャッシュ用配列の作成: O(n) * - 再帰的な探索: O(n * m) -* - 最悪の場合、各金額(n)に対して全てのコイン(m)を試す -* - ただし枝刈りにより実際の計算回数は減少する可能性あり +* - 最悪の場合、各 amount に対して全てのコインを試す * 空間計算量: O(n): * - キャッシュ用配列: O(n) * - 再帰スタック: 最悪の場合 O(n) From 60d27c13f1c3c6c4cea97495d6711b2eb93f1ce2 Mon Sep 17 00:00:00 2001 From: seal_azarashi Date: Wed, 6 Nov 2024 09:03:17 +0900 Subject: [PATCH 3/8] step 3 --- arai60/Dynamic_Programming/coin-change.md | 96 +++++++++++++++++------ 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/arai60/Dynamic_Programming/coin-change.md b/arai60/Dynamic_Programming/coin-change.md index 0c325c9..c4511c4 100644 --- a/arai60/Dynamic_Programming/coin-change.md +++ b/arai60/Dynamic_Programming/coin-change.md @@ -185,18 +185,18 @@ class Solution { ```java /** -* 時間計算量: O(n * m): -* - 入力コインの前処理: O(m) -* - キャッシュ用配列の作成: O(n) -* - スタックを使用した探索: O(n * m) -* - 最悪の場合、各 amount に対して全てのコインを試す -* 空間計算量: O(n * m): -* - キャッシュ用配列: O(n) -* - スタック: 最悪の場合 O(n * m) -* - その他定数計算量の変数等: O(1) -* -* ※ n = amount, m = coins.length -*/ + * 時間計算量: O(n * m): + * - 入力コインの前処理: O(m) + * - キャッシュ用配列の作成: O(n) + * - スタックを使用した探索: O(n * m) + * - 最悪の場合、各 amount に対して全てのコインを試す + * 空間計算量: O(n * m): + * - キャッシュ用配列: O(n) + * - スタック: 最悪の場合 O(n * m) + * - その他定数計算量の変数等: O(1) + * + * ※ n = amount, m = coins.length + */ class Solution { private final int NO_COMBINATION = -1; @@ -246,18 +246,18 @@ class Solution { ```java /** -* 時間計算量: O(n * m): -* - 入力コインの前処理: O(m) -* - キャッシュ用配列の作成: O(n) -* - 再帰的な探索: O(n * m) -* - 最悪の場合、各 amount に対して全てのコインを試す -* 空間計算量: O(n): -* - キャッシュ用配列: O(n) -* - 再帰スタック: 最悪の場合 O(n) -* - その他定数計算量の変数等: O(1) -* -* ※ n = amount, m = coins.length -*/ + * 時間計算量: O(n * m): + * - 入力コインの前処理: O(m) + * - キャッシュ用配列の作成: O(n) + * - 再帰的な探索: O(n * m) + * - 最悪の場合、各 amount に対して全てのコインを試す + * 空間計算量: O(n): + * - キャッシュ用配列: O(n) + * - 再帰スタック: 最悪の場合 O(n) + * - その他定数計算量の変数等: O(1) + * + * ※ n = amount, m = coins.length + */ class Solution { private final int NO_COMBINATION = -1; @@ -301,3 +301,51 @@ class Solution { } } ``` + +## Step 3 + +実装のシンプルさと処理効率から Step 2 に書いた DP の解法を選びました。このステップでは5分以内に書くことを目指すのでコメントや引数の null チェックは省略していますが、実際の面接ではそういったものが必要であることをしっかり補足したいところです。 + +```java +/** + * 解いた時間: 約5分 + * 時間計算量: O(n * m): + * - 入力コインの前処理: O(m) + * - キャッシュ用配列の作成: O(n) + * - キューを使用した探索: O(n * m) + * - 最悪の場合、各 amount に対して全てのコインを試す + * 空間計算量: O(n * m): + * - キャッシュ用配列: O(n) + * - キュー: 最悪の場合 O(n * m) + * - その他定数計算量の変数等: O(1) + * + * ※ n = amount, m = coins.length + */ +class Solution { + private final int NO_COMBINATION = -1; + + public int coinChange(int[] coins, int amount) { + int[] amountToMinCoinCount = new int[amount + 1]; + Arrays.fill(amountToMinCoinCount, NO_COMBINATION); + amountToMinCoinCount[0] = 0; + + for (int currentAmount = 1; currentAmount <= amount; currentAmount++) { + int minCoinCount = Integer.MAX_VALUE; + boolean isAnyCombinationFound = false; + for (int coin : coins) { + int remainder = currentAmount - coin; + if (remainder < 0 || amountToMinCoinCount[remainder] == NO_COMBINATION) { + continue; + } + + minCoinCount = Math.min(minCoinCount, amountToMinCoinCount[remainder] + 1); + isAnyCombinationFound = true; + } + if (isAnyCombinationFound) { + amountToMinCoinCount[currentAmount] = minCoinCount; + } + } + return amountToMinCoinCount[amount]; + } +} +``` From 5d531a64441451c1d1a5b2ef7fbeb4bb898a4998 Mon Sep 17 00:00:00 2001 From: seal_azarashi Date: Thu, 7 Nov 2024 08:43:29 +0900 Subject: [PATCH 4/8] step 4 --- arai60/Dynamic_Programming/coin-change.md | 62 +++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/arai60/Dynamic_Programming/coin-change.md b/arai60/Dynamic_Programming/coin-change.md index c4511c4..83f4996 100644 --- a/arai60/Dynamic_Programming/coin-change.md +++ b/arai60/Dynamic_Programming/coin-change.md @@ -349,3 +349,65 @@ class Solution { } } ``` + +## Step 4 + +[oda さんの指摘](https://github.com/seal-azarashi/leetcode/pull/37#discussion_r1830469401)から、処理の切り上げが上手くいっていなかったことが理解できたので修正した。 + +### BFS + +```java +/** +* 時間計算量: O(n * m): +* - 入力コインの前処理: O(m) +* - キャッシュ用配列の作成: O(n) +* - キューを使用した探索: O(n * m) +* - 最悪の場合、各 amount に対して全てのコインを試す +* 空間計算量: O(n * m): +* - キャッシュ用配列: O(n) +* - キュー: 最悪の場合 O(n * m) +* - その他定数計算量の変数等: O(1) +* +* ※ n = amount, m = coins.length +*/ +class Solution { + private final int NO_COMBINATION = -1; + + private record CoinState(int numCoins, int currentAmount) {} + + public int coinChange(int[] coins, int amount) { + Objects.requireNonNull(coins, "Argument coins must not be null"); + + int[] filteredAndSortedCoins = Arrays.stream(coins) + .filter(coin -> coin <= amount) + .toArray(); + + int[] makeUpAmountToCoinCount = new int[amount + 1]; + Arrays.fill(makeUpAmountToCoinCount, Integer.MAX_VALUE); + + Queue coinStateQueue = new ArrayDeque<>(); + coinStateQueue.offer(new CoinState(0, 0)); + + while (!coinStateQueue.isEmpty()) { + CoinState coinState = coinStateQueue.poll(); + if (makeUpAmountToCoinCount[coinState.currentAmount()] <= coinState.numCoins()) { + continue; + } + + makeUpAmountToCoinCount[coinState.currentAmount()] = coinState.numCoins(); + for (int coin : filteredAndSortedCoins) { + int nextAmount = coinState.currentAmount() + coin; + if (nextAmount > amount) { + continue; + } + + coinStateQueue.offer(new CoinState(coinState.numCoins() + 1, nextAmount)); + } + } + + return makeUpAmountToCoinCount[amount] == Integer.MAX_VALUE + ? NO_COMBINATION + : makeUpAmountToCoinCount[amount]; + } +} +``` From fdad5c218f1187a99b8ea9d5d492adc6ae447a0f Mon Sep 17 00:00:00 2001 From: seal_azarashi Date: Thu, 7 Nov 2024 08:59:49 +0900 Subject: [PATCH 5/8] add step 4 --- arai60/Dynamic_Programming/coin-change.md | 62 +++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/arai60/Dynamic_Programming/coin-change.md b/arai60/Dynamic_Programming/coin-change.md index 83f4996..b0622f7 100644 --- a/arai60/Dynamic_Programming/coin-change.md +++ b/arai60/Dynamic_Programming/coin-change.md @@ -411,3 +411,65 @@ class Solution { } } ``` + +### 非再帰の DFS + +[oda さんの指摘](https://github.com/seal-azarashi/leetcode/pull/37#discussion_r1830471177)から、より早く少ない枚数の組み合わせが見つかるよう、大きい額のコインからチェックされるように修正。処理時間が 1/6 ほどに減少。 + +```java +/** + * 時間計算量: O(n * m): + * - 入力コインの前処理: O(m) + * - キャッシュ用配列の作成: O(n) + * - スタックを使用した探索: O(n * m) + * - 最悪の場合、各 amount に対して全てのコインを試す + * 空間計算量: O(n * m): + * - キャッシュ用配列: O(n) + * - スタック: 最悪の場合 O(n * m) + * - その他定数計算量の変数等: O(1) + * + * ※ n = amount, m = coins.length + */ +class Solution { + private final int NO_COMBINATION = -1; + + private record CoinState(int numCoins, int currentAmount) {} + + public int coinChange(int[] coins, int amount) { + Objects.requireNonNull(coins, "Argument coins must not be null"); + + int[] filteredCoins = Arrays.stream(coins) + .filter(coin -> coin <= amount) + .sorted() + .toArray(); + + int[] makeUpAmountToCoinCount = new int[amount + 1]; + Arrays.fill(makeUpAmountToCoinCount, Integer.MAX_VALUE); + + Stack coinStateStack = new Stack<>(); + coinStateStack.push(new CoinState(0, 0)); + + while (!coinStateStack.isEmpty()) { + CoinState coinState = coinStateStack.pop(); + makeUpAmountToCoinCount[coinState.currentAmount()] = Math.min( + makeUpAmountToCoinCount[coinState.currentAmount()], + coinState.numCoins() + ); + for (int coin : filteredCoins) { + int nextAmount = coinState.currentAmount() + coin; + if (nextAmount > amount) { + continue; + } + if (makeUpAmountToCoinCount[nextAmount] <= coinState.numCoins() + 1) { + continue; + } + coinStateStack.push(new CoinState(coinState.numCoins() + 1, nextAmount)); + } + } + + return makeUpAmountToCoinCount[amount] == Integer.MAX_VALUE + ? NO_COMBINATION + : makeUpAmountToCoinCount[amount]; + } +} +``` From e63c235e4f484f8662894b816586e1d2da12ac85 Mon Sep 17 00:00:00 2001 From: seal_azarashi Date: Thu, 7 Nov 2024 09:03:11 +0900 Subject: [PATCH 6/8] add step 4 --- arai60/Dynamic_Programming/coin-change.md | 65 +++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/arai60/Dynamic_Programming/coin-change.md b/arai60/Dynamic_Programming/coin-change.md index b0622f7..9622918 100644 --- a/arai60/Dynamic_Programming/coin-change.md +++ b/arai60/Dynamic_Programming/coin-change.md @@ -473,3 +473,68 @@ class Solution { } } ``` + +### 再帰の DFS + +[oda さんの指摘](https://github.com/seal-azarashi/leetcode/pull/37#discussion_r1830471177)から、より早く少ない枚数の組み合わせが見つかるよう、大きい額のコインからチェックされるように修正。処理時間が 1/15 ほどに減少。 + +```java +/** + * 時間計算量: O(n * m): + * - 入力コインの前処理: O(m) + * - キャッシュ用配列の作成: O(n) + * - 再帰的な探索: O(n * m) + * - 最悪の場合、各 amount に対して全てのコインを試す + * 空間計算量: O(n): + * - キャッシュ用配列: O(n) + * - 再帰スタック: 最悪の場合 O(n) + * - その他定数計算量の変数等: O(1) + * + * ※ n = amount, m = coins.length + */ +class Solution { + private final int NO_COMBINATION = -1; + + public int coinChange(int[] coins, int amount) { + Objects.requireNonNull(coins, "Argument coins must not be null"); + + int[] filteredCoins = Arrays.stream(coins) + .filter(coin -> coin <= amount) + .boxed() + .sorted(Collections.reverseOrder()) + .mapToInt(Integer::intValue) + .toArray(); + + int[] makeUpAmountToCoinCount = new int[amount + 1]; + Arrays.fill(makeUpAmountToCoinCount, Integer.MAX_VALUE); + makeUpAmountToCoinCount[0] = 0; + + updateMinCoinCountsRecursively(filteredCoins, amount, 0, 0, makeUpAmountToCoinCount); + + return makeUpAmountToCoinCount[amount] == Integer.MAX_VALUE + ? NO_COMBINATION + : makeUpAmountToCoinCount[amount]; + } + + private void updateMinCoinCountsRecursively( + int[] coins, + int targetAmount, + int currentAmount, + int numCoins, + int[] makeUpAmountToCoinCount + ) { + makeUpAmountToCoinCount[currentAmount] = Math.min(makeUpAmountToCoinCount[currentAmount], numCoins); + + for (int coin : coins) { + int nextAmount = currentAmount + coin; + if (nextAmount > targetAmount) { + continue; + } + if (makeUpAmountToCoinCount[nextAmount] <= numCoins + 1) { + continue; + } + updateMinCoinCountsRecursively(coins, targetAmount, nextAmount, numCoins + 1, makeUpAmountToCoinCount); + } + } +} +``` From d8a426b23fa6b3e2addcb17a56d81ea58a42df99 Mon Sep 17 00:00:00 2001 From: seal_azarashi Date: Fri, 8 Nov 2024 07:21:11 +0900 Subject: [PATCH 7/8] fix --- arai60/Dynamic_Programming/coin-change.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arai60/Dynamic_Programming/coin-change.md b/arai60/Dynamic_Programming/coin-change.md index 9622918..7f64ade 100644 --- a/arai60/Dynamic_Programming/coin-change.md +++ b/arai60/Dynamic_Programming/coin-change.md @@ -352,10 +352,10 @@ class Solution { ## Step 4 -[oda さんの指摘](https://github.com/seal-azarashi/leetcode/pull/37#discussion_r1830469401)から、処理の切り上げが上手くいっていなかったことが理解できたので修正した。 - ### BFS +[oda さんの指摘](https://github.com/seal-azarashi/leetcode/pull/37#discussion_r1830469401)から、処理の切り上げが上手くいっていなかったことが理解できたので修正した。 + ```java /** * 時間計算量: O(n * m): From 3a99d932f3275ac142a33ec55946c5bf5596a7f4 Mon Sep 17 00:00:00 2001 From: seal_azarashi Date: Fri, 8 Nov 2024 07:30:36 +0900 Subject: [PATCH 8/8] add step 4 --- arai60/Dynamic_Programming/coin-change.md | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/arai60/Dynamic_Programming/coin-change.md b/arai60/Dynamic_Programming/coin-change.md index 7f64ade..18a068c 100644 --- a/arai60/Dynamic_Programming/coin-change.md +++ b/arai60/Dynamic_Programming/coin-change.md @@ -352,6 +352,43 @@ class Solution { ## Step 4 +### 1 から amount まで、それぞれ何枚のコインで作れるかをキャッシュする DP + +次の nodchip さんの案を踏まえて修正した: + +- https://github.com/seal-azarashi/leetcode/pull/37#discussion_r1832794636 +- https://github.com/seal-azarashi/leetcode/pull/37#discussion_r1832796152 + +```java +class Solution { + private final int CANNOT_BE_MADE_UP = Integer.MAX_VALUE; + private final int NO_COMBINATION = -1; + + public int coinChange(int[] coins, int amount) { + int[] amountToMinCoins = new int[amount + 1]; + Arrays.fill(amountToMinCoins, CANNOT_BE_MADE_UP); + amountToMinCoins[0] = 0; + + for (int currentAmount = 1; currentAmount <= amount; currentAmount++) { + for (int coin : coins) { + int remainder = currentAmount - coin; + if (remainder < 0 || amountToMinCoins[remainder] == CANNOT_BE_MADE_UP) { + continue; + } + + amountToMinCoins[currentAmount] = Math.min( + amountToMinCoins[currentAmount], + amountToMinCoins[remainder] + 1 + ); + } + } + return amountToMinCoins[amount] == CANNOT_BE_MADE_UP + ? NO_COMBINATION + : amountToMinCoins[amount]; + } +} +``` + ### BFS [oda さんの指摘](https://github.com/seal-azarashi/leetcode/pull/37#discussion_r1830469401)から、処理の切り上げが上手くいっていなかったことが理解できたので修正した。