diff --git a/Python3/322. Coin Change.md b/Python3/322. Coin Change.md new file mode 100644 index 0000000..2d61136 --- /dev/null +++ b/Python3/322. Coin Change.md @@ -0,0 +1,131 @@ +## Step 1. Initial Solution + +- 順番に大きい方から割れるだけ割って、残りを引き継いでいく方法 + - 辞書型に残りの金額と使った枚数を保持 + - やっぱりリストで十分なので修正 + - 明らかに計算コストは高いが他に思いつかなかったのでこれで実装 + - コインの枚数は12なのでsortやcoinのループ数自体は大きな問題にはならないが辞書のコピーとその各要素に対する実行で最大O(amount ^ 2) + - 更新のタイミングが難しく、サンプルケースを通す実装に1時間かけてしまった + +```python +class Solution: + def coinChange(self, coins: List[int], amount: int) -> int: + remaining_to_min_coin = [None] * amount + [0] + for remaining in range(amount, 0, -1): + if remaining_to_min_coin[remaining] is None: continue + for coin in coins: + if coin > remaining: continue + for i in range(remaining // coin, 0, -1): + if remaining_to_min_coin[remaining - coin * i] is None: + remaining_to_min_coin[remaining - coin * i] = remaining_to_min_coin[remaining] + i + continue + remaining_to_min_coin[remaining - coin * i] = \ + min(remaining_to_min_coin[remaining - coin * i], remaining_to_min_coin[remaining] + i) + if remaining_to_min_coin[0] is None: + return -1 + return remaining_to_min_coin[0] +``` + +- ここからさらにしばらく考えてみたが、よく考えたら各ステップでそのコインがたどり着ける場所を全部考慮する必要はないことに気が付いた + - シンプルに各ステップでは1枚コインを追加して行ける場所だけ記録すればよい + - これで無事に通った + + ```python + class Solution: + def coinChange(self, coins: List[int], amount: int) -> int: + remaining_to_min_coin = [None] * amount + [0] + for remaining in range(amount, 0, -1): + if remaining_to_min_coin[remaining] is None: continue + for coin in coins: + if coin > remaining: continue + if remaining_to_min_coin[remaining - coin] is None: + remaining_to_min_coin[remaining - coin] = remaining_to_min_coin[remaining] + 1 + continue + remaining_to_min_coin[remaining - coin] = \ + min(remaining_to_min_coin[remaining - coin], remaining_to_min_coin[remaining] + 1) + if remaining_to_min_coin[0] is None: + return -1 + return remaining_to_min_coin[0] + ``` + + +### Complexity Analysis + +- 時間計算量:O(n * k) + - コインの枚数nと総額k +- 空間計算量:O(k) + - 総額分の長さのリストを利用 + +## Step 2. Alternatives + +- math.infを初期値に使うこともできる + - 最後の判定にmath.isinfを用いると綺麗に書ける + - https://github.com/tokuhirat/LeetCode/pull/40/files#diff-8f6b81efa221a5236aad48fc3551b5d0288080123ad0271a4536d46d0c207302R62 +- 再帰で解く方法 + - cacheが自然に出てきて欲しい + - lru_cacheを使うならサイズを気にしたい + - https://github.com/Fuminiton/LeetCode/pull/40/files#r2073709258 + - 始めは以下の実装でamount==coinの判定をしていたが==0にすれば例外処理が不要になることに気づいた + + ```python + class Solution: + def coinChange(self, coins: List[int], amount: int) -> int: + @cache + def numCoinsToAmount(amount: int) -> int: + min_coins = math.inf + for coin in coins: + if amount < 0: + continue + if amount == 0: + return 0 + min_coins = \ + min(min_coins, numCoinsToAmount(amount - coin) + 1) + return min_coins + num_coins = numCoinsToAmount(amount) + if isinf(num_coins): + return -1 + return num_coins + ``` + + - 最小値を求めるときにGeneratorを使うパターンもあるらしい + - https://github.com/TORUS0818/leetcode/pull/42/files#r1904039471 + + ```python + class Solution: + def coinChange(self, coins: List[int], amount: int) -> int: + @cache + def numCoinsToAmount(amount: int) -> int: + if amount == 0: + return 0 + if amount < 0: + return -1 + def listNumCoinsPossible(): + for coin in coins: + num_coins = numCoinsToAmount(amount - coin) + if num_coins == -1: + continue + yield num_coins + 1 + return min(listNumCoinsPossible(), default=-1) + return numCoinsToAmount(amount) + ``` + + +## Step 3. Final Solution + +- 再帰も悪くない気がしたが、今回はDP最後の問題ということで練習しておいた + +```python +class Solution: + def coinChange(self, coins: List[int], amount: int) -> int: + min_coins_to_amount = [0] + [-1] * amount + for i, num_coins in enumerate(min_coins_to_amount): + if num_coins == -1: + continue + for coin in coins: + if i + coin > amount: + continue + if min_coins_to_amount[i + coin] == -1 or \ + min_coins_to_amount[i + coin] > num_coins + 1: + min_coins_to_amount[i + coin] = num_coins + 1 + return min_coins_to_amount[-1] +```