-
Notifications
You must be signed in to change notification settings - Fork 0
322. Coin Change #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
322. Coin Change #38
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| start_new_problem.sh | ||
| main.go | ||
| go.mod | ||
| go.sum | ||
| *.go |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,302 @@ | ||
| 問題: https://leetcode.com/problems/coin-change/description/ | ||
|
|
||
| ### Step 1 | ||
| - n円払うのに必要な最小の硬貨数を1から順に計算して記録していく | ||
| - n円払いたい。 | ||
| n-1円を20枚の硬貨で出し終えていて1円玉があったら21枚で支払える。 | ||
| 一方、n-5円を10枚の硬貨で出せていたら11枚で足りる。 | ||
| - テストケース | ||
| - coins=[], amount=10 -> -1 | ||
| - coins=[1,5,10], amount=0 -> 0 | ||
| - coins=[1], amount=10 -> 10 | ||
| - coins=[2], amount=10 -> 5 | ||
| - coins=[3], amount=10 -> -1 | ||
| - coins=[1,5,10], amount=6 -> 2 | ||
| - coins=[1,5,10], amount=9 -> 5 | ||
| - coins=[1,5,10], amount=16 -> 3 | ||
| - coins=[1,5,10], amount=25 -> 3 | ||
| - coins=[3,5,10], amount=7 -> -1 | ||
| - 制約条件で`1 <= coins[i] <= 2^31 - 1`となっていることから | ||
| integer overflowが起きないか心配になった。 | ||
| coin[i1]+coin[i2]みたいな計算はないので大丈夫そう | ||
| - Goのint型は32bitマシンでは32bit, 64bitマシンでは64bit | ||
| - ループの中のif文が多くて美しくない | ||
| - ループの前後のエッジケース処理が美しくない | ||
| - 入出力の合致するコードになっただけ感 | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 0が三重の意味になっているのもややわかりにくそうですね |
||
| ```Go | ||
| func coinChange(coins []int, amount int) int { | ||
| if amount == 0 { | ||
| return 0 | ||
| } | ||
| minCoinCountsForAmount := make([]int, amount+1) | ||
| for amt := 1; amt <= amount; amt++ { | ||
| for _, coin := range coins { | ||
| if amt-coin < 0 { | ||
| continue | ||
| } | ||
| if amt-coin == 0 { | ||
| minCoinCountsForAmount[amt] = 1 | ||
| break | ||
| } | ||
| if minCoinCountsForAmount[amt-coin] == 0 { | ||
| continue | ||
| } | ||
| if minCoinCountsForAmount[amt] == 0 { | ||
| minCoinCountsForAmount[amt] = minCoinCountsForAmount[amt-coin] + 1 | ||
| continue | ||
| } | ||
| minCoinCountsForAmount[amt] = min(minCoinCountsForAmount[amt], minCoinCountsForAmount[amt-coin]+1) | ||
| } | ||
| } | ||
| if minCoinCountsForAmount[amount] == 0 { | ||
| return -1 | ||
| } | ||
| return minCoinCountsForAmount[amount] | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 2 | ||
| #### 2a | ||
| - step1のボトムアップDPの改善 | ||
| - DPテーブルをmath.MaxIntで埋めれば条件分岐が少なくなる | ||
| - 二つのループを合体させることもできるが、DPテーブルの初期化をまとめて行った方がわかりやすいと思い、ループを分けた | ||
| - 最初のループはJavaのArrays.fillのようなメソッドがGoにあれば生まれなかった | ||
| - 参考 | ||
| - https://hayapenguin.com/notes/LeetCode/322/CoinChange | ||
| - https://github.com/seal-azarashi/leetcode/pull/37/files#r1832794636 | ||
|
|
||
| ```Go | ||
| func coinChange(coins []int, amount int) int { | ||
| minCoinCombinations := make([]int, amount+1) | ||
| for i := 1; i < len(minCoinCombinations); i++ { | ||
| minCoinCombinations[i] = math.MaxInt64 | ||
| } | ||
| for amt := 1; amt <= amount; amt++ { | ||
| for _, coin := range coins { | ||
| if coin <= amt && minCoinCombinations[amt-coin] < math.MaxInt64 { | ||
| minCoinCombinations[amt] = min(minCoinCombinations[amt], minCoinCombinations[amt-coin]+1) | ||
| } | ||
| } | ||
| } | ||
| if minCoinCombinations[amount] == math.MaxInt64 { | ||
| return -1 // no combination exist | ||
| } | ||
| return minCoinCombinations[amount] | ||
| } | ||
| ``` | ||
|
|
||
| #### 2b | ||
| - 2aのminCoinCombinationsのようなDPテーブルに、 | ||
| その金額を払うための最小coin数と払えるかどうか(math.MaxInt64なら払えない) | ||
| というheterogeneousな情報が格納されてしまうのは良くないというコメントがあった | ||
| - https://github.com/goto-untrapped/Arai60/pull/34#discussion_r1668488602 | ||
| - 支払い可能な金額をmapで管理するようにした | ||
| - 2aが10ms程度だったのに対し、こちらは200ms程度になった | ||
| - 下記コードだと再ハッシュされるので、 | ||
| `changeableAmounts := make(map[int]struct{}, amount+1)` | ||
| とサイズを設定しても100msくらいかかった | ||
| - mapへの書き込みが思い? | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. はい。思ったよりもハッシュ値の計算遅いんですよね。それに対して、配列の場合はポインター+足し算になり、CPU によっては、LEA など一命令でできるものもあります。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LEAについて初耳だったので調べてみました。LEAはLoad Effective Addressの略でx86アーキテクチャのIntelやAMDにある命令で |
||
|
|
||
| ```Go | ||
| func coinChange(coins []int, amount int) int { | ||
| coinCountForAmounts := make([]int, amount+1) | ||
| for i := 1; i < len(coinCountForAmounts); i++ { | ||
| coinCountForAmounts[i] = math.MaxInt64 | ||
| } | ||
| changeableAmounts := map[int]struct{}{0: {}} | ||
| for currentAmount := 1; currentAmount <= amount; currentAmount++ { | ||
| for _, coin := range coins { | ||
| if currentAmount < coin { | ||
| continue | ||
| } | ||
| if _, found := changeableAmounts[currentAmount-coin]; !found { | ||
| continue | ||
| } | ||
| coinCountForAmounts[currentAmount] = min(coinCountForAmounts[currentAmount], coinCountForAmounts[currentAmount-coin]+1) | ||
| changeableAmounts[currentAmount] = struct{}{} | ||
| } | ||
| } | ||
| if _, found := changeableAmounts[amount]; !found { | ||
| return -1 // the given amount is not changeable | ||
| } | ||
| return coinCountForAmounts[amount] | ||
| } | ||
| ``` | ||
|
|
||
| #### 2c | ||
| - coinsを降順にソートしてDFS | ||
| - 12,345円払わないといけない時に1円玉から考えたりしない。 | ||
| 1万円札が1枚必要で、千円札が2枚必要で、...と大きい額のものから順に考えていくはず | ||
|
|
||
| - 最初に以下のようなコードを書いてWA | ||
| - これだとgreedyなので、coins=[1,8,10], amount=16のような場合に | ||
| 10+1*6に飛びついてしまう(それ以上の探索をせずに回答してしまう) | ||
|
|
||
| ```Go | ||
| func coinChange(coins []int, amount int) int { | ||
| // sortedCoins is a slice of coins sorted in the descending order | ||
| sortedCoins := make([]int, len(coins)) | ||
| copy(sortedCoins, coins) | ||
| slices.SortFunc(sortedCoins, func(a, b int) int { return b - a }) | ||
|
|
||
| return coinChangeHelper(sortedCoins, amount) | ||
| } | ||
|
|
||
| // Argument sortedCoins is sorted in the descending order. | ||
| func coinChangeHelper(sortedCoins []int, amount int) int { | ||
| if amount == 0 { | ||
| return 0 | ||
| } | ||
| for i, coin := range sortedCoins { | ||
| if coin > amount { | ||
| continue | ||
| } | ||
| coinCount := coinChangeHelper(sortedCoins[i:], amount-coin) + 1 | ||
| if coinCount > 0 { | ||
| return coinCount | ||
| } | ||
| } | ||
| return -1 | ||
| } | ||
| ``` | ||
|
|
||
| - これがACコード | ||
| - coinsを降順にソートする際は参照透過性を保つためにcopyした | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 好みですが、自分は配列に保存して後でamountの時のコインの数探るよいうより、L184~のループの中でamountになった時点でその時点でのcoinの数を出力するコードにします。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. これやると貪欲と一緒になっちゃいますね(何度もすみません) |
||
| ```Go | ||
| func coinChange(coins []int, amount int) int { | ||
| // sortedCoins is sorted in the descending order | ||
| sortedCoins := make([]int, len(coins)) | ||
| copy(sortedCoins, coins) | ||
| slices.SortFunc(sortedCoins, func(a, b int) int { | ||
| return b - a | ||
| }) | ||
|
|
||
| minCoinCountForAmounts := make([]int, amount+1) | ||
| for i := 1; i < len(minCoinCountForAmounts); i++ { | ||
| minCoinCountForAmounts[i] = math.MaxInt64 | ||
| } | ||
|
|
||
| var coinChangeHelper func(accumulatedAmount int, coinCount int, sortedCoinsStartIndex int) | ||
| coinChangeHelper = func(accumulatedAmount, coinCount, sortedCoinsStartIndex int) { | ||
| minCoinCountForAmounts[accumulatedAmount] = min(minCoinCountForAmounts[accumulatedAmount], coinCount) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ここはminCoinCountForAmounts[accumulatedAmount] = coinCountでも大丈夫ですかね(L189-L190で弾いてるので)
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 本当ですね。ありがとうございます! |
||
| for i := sortedCoinsStartIndex; i < len(sortedCoins); i++ { | ||
| nextAmount := accumulatedAmount + sortedCoins[i] | ||
| if nextAmount > amount { | ||
| continue | ||
| } | ||
| if minCoinCountForAmounts[nextAmount] <= coinCount+1 { | ||
| continue | ||
| } | ||
| coinChangeHelper(nextAmount, coinCount+1, i) | ||
| } | ||
| } | ||
|
|
||
| coinChangeHelper(0, 0, 0) | ||
| if minCoinCountForAmounts[amount] < math.MaxInt64 { | ||
| return minCoinCountForAmounts[amount] | ||
| } | ||
| return -1 // no coin combination found | ||
| } | ||
| ``` | ||
|
|
||
| #### 2d | ||
| - BFS | ||
| - 無名構造体の書き方を忘れていた | ||
| - 参考 | ||
| - https://github.com/seal-azarashi/leetcode/pull/37/files#r1830469401 | ||
| - https://github.com/seal-azarashi/leetcode/pull/37/files#diff-d111aa48a9b8e9e14eb6bc7f1e3254ae69a49b49fc338da8095b0b21ce3ad667R410 | ||
|
|
||
| ```Go | ||
| func coinChange(coins []int, amount int) int { | ||
| amountToMinCoinCount := make([]int, amount+1) | ||
| for i := range amountToMinCoinCount { | ||
| amountToMinCoinCount[i] = math.MaxInt64 | ||
| } | ||
| queue := []struct { | ||
| amount int | ||
| coinCount int | ||
| }{{0, 0}} | ||
|
|
||
| for len(queue) > 0 { | ||
| head := queue[0] | ||
| currentAmount := head.amount | ||
| currentCoinCount := head.coinCount | ||
| queue = queue[1:] | ||
| if amountToMinCoinCount[currentAmount] <= currentCoinCount { | ||
| continue | ||
| } | ||
| amountToMinCoinCount[currentAmount] = currentCoinCount | ||
| for _, coin := range coins { | ||
| if head.amount+coin > amount { | ||
| continue | ||
| } | ||
| queue = append(queue, struct { | ||
| amount int | ||
| coinCount int | ||
| }{currentAmount + coin, currentCoinCount + 1}) | ||
| } | ||
| } | ||
| if amountToMinCoinCount[amount] == math.MaxInt64 { | ||
| return -1 // no combination exist | ||
| } | ||
| return amountToMinCoinCount[amount] | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 3 | ||
| - coinsの降順ソートしてDFSする方法が最も直感に近い方法な気がした | ||
| - 2cからの改善点 | ||
| - math.MaxInt64がマジックナンバーにならないよう定数notChangeableを用意する | ||
| - coinChangeHelperだとヘルパー関数であるという情報しかないので、 | ||
| searchMinCoinCountsに変更 | ||
| - 参照透過性を保つためにsortedCoinsという新しいsliceを作ったつもりだったが、 | ||
| よく考えたら少なくともcoinChange関数においてcoinsをin-placeにソートしても | ||
| 参照透過性は損なわれない。 | ||
| coinChangeの呼び出し元でcoinsの順番に意味がある場合にソートするのは問題かもしれないが、 | ||
| coinsの順番に意味はないと勝手に判断し、in-placeでソートすることに | ||
|
|
||
| ```Go | ||
| func coinChange(coins []int, amount int) int { | ||
| const notChangeable = -1 | ||
|
|
||
| // sort coins in the descending order | ||
| slices.SortFunc(coins, func(a, b int) int { return b - a }) | ||
|
|
||
| coinCountForAmounts := make([]int, amount+1) | ||
| for i := 1; i < len(coinCountForAmounts); i++ { | ||
| coinCountForAmounts[i] = notChangeable | ||
| } | ||
|
|
||
| var searchMinCoinCounts func(currentAmount int, coinCount int, coinsStartIndex int) | ||
| searchMinCoinCounts = func(currentAmount, coinCount, coinsStartIndex int) { | ||
| coinCountForAmounts[currentAmount] = coinCount | ||
| for i := coinsStartIndex; i < len(coins); i++ { | ||
| nextAmount := currentAmount + coins[i] | ||
| if nextAmount > amount { | ||
| continue | ||
| } | ||
| if coinCountForAmounts[nextAmount] != notChangeable && coinCountForAmounts[nextAmount] <= coinCount+1 { | ||
| continue | ||
| } | ||
| searchMinCoinCounts(nextAmount, coinCount+1, i) | ||
| } | ||
| } | ||
|
|
||
| searchMinCoinCounts(0, 0, 0) | ||
| return coinCountForAmounts[amount] | ||
| } | ||
| ``` | ||
|
|
||
| ### CS | ||
| - composite literal | ||
| - 複数のデータ型を一つの集合として表すもの | ||
| - ex. スライス、構造体、マップ、チャネル | ||
| - プリミティブ型の対義語のイメージ | ||
| - simplifycompositelit | ||
| - コンポジット表現の簡素化をするフォーマッター | ||
| - gofmt -s で呼び出せる。vscodeでセーブ時に動いてくれてるっぽい | ||
| - `changeableAmounts := map[int]struct{}{0: struct{}{}}` | ||
| としたらsimplifycompositelitに怒られ、 | ||
| `changeableAmounts := map[int]struct{}{0: {}}`に直された | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
価格が等しいコインがいくつかある場合のテストケースは欲しいかもしれません
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
すみません、問題文の要件を勘違いしてました