Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions Python3/322. Coin Change.md
Original file line number Diff line number Diff line change
@@ -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]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同一の list の中に、 None と 0 という異なる型の値が含まれている点に違和感を感じました。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そうなんですね、同じ型で比較できる形の方が自然なんですね

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remaining_to_min_coin = [None] * (amount + 1)
remaining_to_min_coin[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):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indexでないものにiを使うのはやや違和感があるかもしれません

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:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://google.github.io/styleguide/pyguide.html#316-naming
LeetCodeは何故か従ってないのですが、Pythonの関数名はnum_coins_to_amountのような表記が一般的そうです。
チームのルールによって異なるとは思います。

min_coins = math.inf
for coin in coins:
if amount < 0:
continue
if amount == 0:
return 0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amountのチェックはfor分岐の前に持ってきたほうが自然でしょうか。

min_coins = \
min(min_coins, numCoinsToAmount(amount - coin) + 1)
return min_coins
num_coins = numCoinsToAmount(amount)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

関数の切れ目が認識しづらいため、個人的には inner function のあとに空行を入れたいです。

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:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この条件分岐の方法は思いつきませんでした。minなどの比較をしなくてよくなるのでシンプルになっていいですね!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

min_coins_to_amount の値を大きな数字で初期化すれば、 if 文が不要になると思います。

INFINITY = 100000
min_coins_to_amount = [0] + [INFINITY] * 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
        min_coins_to_amount[i + coin] = min(min_coins_to_amount[i + coin], num_coins + 1)
if min_coins_to_amount[-1] == INFINITY:
    return -1
return min_coins_to_amount[-1]

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確かに最後に一回分岐作るだけの方が良さそうですね

min_coins_to_amount[i + coin] = num_coins + 1
return min_coins_to_amount[-1]
```