-
Notifications
You must be signed in to change notification settings - Fork 0
322 coin change medium #17
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?
Conversation
| ```python | ||
| class Solution: | ||
| def coinChange(self, coins: list[int], amount: int) -> int: | ||
| INFINITY = amount + 1 |
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.
amount + 1 とするのは良いと思いました。自分は、問題の制約からMAX_INT = 10 ** 4 + 1とおいてしましたが、こちらの方が最適かと思います。変数名、INFINITYは、無限を表わしているわけではないので、実運用ではコメントがあると親切かと思いました。
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.
数字の場合は、だんだんいろいろな事情で大きな数字が入るようになってきて、気がついたら、ここにその数が流れてきて、10001 を超えていたという事が起きるでしょう。で、事故を起こして、顧客に代表が謝罪をしているときに、2年前にこのコードを書いたときには、10000までしか来ないって言われていたので、このコードは悪くありません、となるかということですね。
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.
レビューありがとうございます。
確かにコメントが無いとなぜINFINITYなのかわかりづらいかと思ったので、以下のコメントを追記しました。
...
# Number of coins does not exceed amount because minimum value of a coin is one, which means this INFINITY value works as an upper bound of number of coins.
INFINITY = amount + 1
...
|
|
||
| ### 発見 | ||
| - `float("inf")`はIEEE 754で定義された特別な値で、無限大を表す。そのため、近似値でもなく、誤差もない。しかし、配列の各要素の更新の処理で、floatとintの比較や演算が入ることを避ける(type checkが入ると型安全でないとwarningが出る)ため、今回は`amount + 1`を代わりに使用した。 | ||
| - `min(Iterable arg)`関数には`default`という引数があり、これは`arg`が空のときに返す値を指定できる。これを指定せずに空リストを`min`に渡すと`ValueError`になる。 |
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.
全体的に参照したドキュメントのリンクがあると良いなと思いました。
https://docs.python.org/3/library/functions.html#min
min(iterable, /, *, default, key=None)
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.
確かに情報のソースには気をつけます。ありがとうございます。
| `step2_topdown_dp.py` として実装。 | ||
|
|
||
| ```python | ||
| from functools import cache |
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.
こちらも、下では functools.lru_cache と書かれていますが違いをドキュメントなどで確認して使われたのでしょうか?すでにご存じでしたらすみません。
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.
ご指摘ありがとうございます。
cacheデコレータはLRUキャッシュの簡易版でキャッシュの期限切れを設定しないものという認識です。プロダクションレベルの話ではfunctools.lru_cacheの方が適切かな、と思って書いていましたが、あまり深く調べられていないので、調べてみます。きっかけをありがとうございます。
問題へのリンク
322. Coin Change - LeetCode
言語
Python
問題の概要
与えられた種類のコインを使って、特定の金額を作るために必要な最小のコイン枚数を求める問題である。作ることが不可能な場合は
-1を返す。これは動的計画法(DP)の典型的な問題である。自分の解法
Step1: 初期実装
まず、何も見ずに自力で解いた最初のバージョンである。
実装のポイント
アルゴリズム: ボトムアップの動的計画法(DP)を採用した。
dp[i]を「金額iを作るための最小コイン枚数」と定義し、dp[0]から順番にテーブルを埋めていく。状態遷移式:
dp[i] = min(dp[i], dp[i - coin] + 1)という基本的なDPの状態遷移式を実装している。無限大の表現: 到達不可能な状態を示す値として、問題の制約から十分大きいと考えられる
10**5をINFINITYとして使用した。冗長なロジック: 内側のループで
min_amount_sofarという一時変数を使い、ループの最後でmin()を再度呼び出すなど、コードに冗長な部分が見られる。時間計算量:
O(S*N)(S:amount, N:len(coins))空間計算量:
O(S)Step2:
Step1からの変更点
INFINITYの値を、ハードコードされたマジックナンバー10**5からamount + 1に変更した。コインの最小値は1であるため、コインの枚数がamountを超えることはありえない。したがって、amount + 1は「到達不可能」を示すセンチネル値として機能し、問題の制約が変わってもコードの堅牢性が保たれる。min_amount_sofarを廃止し、DPテーブルmin_num_coinsを直接更新するようにした。これにより、内側のループが状態遷移式そのものになり、コードがより直感的になった。amount_をcurrent_amountに変更し、可読性を向上させた。発見
float("inf")はIEEE 754で定義された特別な値で、無限大を表す。そのため、近似値でもなく、誤差もない。しかし、配列の各要素の更新の処理で、floatとintの比較や演算が入ることを避ける(type checkが入ると型安全でないとwarningが出る)ため、今回はamount + 1を代わりに使用した。min(Iterable arg)関数にはdefaultという引数があり、これはargが空のときに返す値を指定できる。これを指定せずに空リストをminに渡すとValueErrorになる。Step3
previous_amount = current_amount - coinと置いた方が可読性が上がる気がする。別解・模範解答
トップダウンDP (メモ化再帰)
step2_topdown_dp.pyとして実装。実装のポイント
再帰関数と
functools.cacheデコレータ(メモ化)を用いてトップダウンで問題を解く。amountから開始し、必要なサブプロブレム(より小さい金額)だけを計算する。ボトムアップに比べ、コードが問題の再帰的な構造を素直に表現しているため、思考プロセスが自然である。
時間計算量:
O(S*N)空間計算量:
O(S)(再帰の深さ + メモ化テーブル)BFS (幅優先探索)
実装のポイント
この問題を、状態遷移のグラフにおける最短経路問題として捉える解法である。
各金額をグラフのノードと見なし、
amountから0までの最短経路(=最小枚数)をBFSで探索する。queueには(現在の金額, これまでのコイン枚数)のタプルを格納する。visitedセットを用いて、同じ金額を再度探索する無駄を省く。時間計算量:
O(S*N)(最悪ケースでは全状態を訪れる)空間計算量:
O(S)(queueとvisitedセットのサイズ)想定されるフォローアップ質問
CS 基礎
「もしコインの種類が非常に多く、金額
amountが比較的小さい場合、ボトムアップとトップダウンのDPでは、どちらのアプローチが有利になるか?その逆はどうか?」amountより大きく、計算に寄与しない場合、それらのコインを使った計算パスは探索されず、無駄な計算を大幅に削減できる。一方、ボトムアップは全ての金額について全てのコインを試すため、無駄な計算が多くなる。RecursionErrorを引き起こすリスクがある。また、関数呼び出しのオーバーヘッドも無視できなくなる。ボトムアップは単純なループであるため、これらの問題がなく安定して動作する。システム設計
「この
coinChange関数を、実際のPOSシステムのお釣り計算エンジンとして組み込むとします。この関数は1日に何百万回も呼び出されます。パフォーマンスを最大化するために、どのようなシステムレベルの最適化を検討しますか?」キャッシュ戦略:
(coins, amount)の組み合わせでの呼び出し結果をキャッシュする。functools.lru_cacheのようなインメモリキャッシュが有効である。DPの計算途中の結果もキャッシュされるため効果的。coins)が変更された場合(例: 新しい記念硬貨の追加)、キャッシュ全体を無効化(パージ)する必要がある。前処理と定数倍最適化:
coins配列をソートしておく。そして、内側のループでcurrent_amount < coinとなったら、それ以降のコインは明らかに大きすぎるため、ループをbreakで早期に打ち切る。これにより、平均的な実行時間を改善できる。次に解く問題の予告