-
Notifications
You must be signed in to change notification settings - Fork 0
Create 1011-capacity-to-ship-packages-within-d-days.md #27
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?
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,155 @@ | ||
| ## Step 1 | ||
|
|
||
| - 問題文 | ||
| - `weights`は積荷の重さが整数で並んでおり、この順で船に積み込み、`days`日かけて全てを運ぶ。 | ||
| - このとき、船のキャパシティの最小値を返す問題。 | ||
| - キャパシティとは1日で運べる最大の積荷の量である。 | ||
| - 制約: | ||
| - 1 <= days <= weights.length <= 5 * 10^4 | ||
| - 1 <= weights[i] <= 500 | ||
| - アルゴリズムの選択 | ||
| - イメージできないので、この問題を手作業で取り組むことを考える。 | ||
| - まず、weightsの全体を見ないとcapacityの見積もりができない。 | ||
| - 最低でもmax(weights)以上でないと、運べない荷物が発生してしまう。 | ||
| - どんなに効率的に運んでも match.ceil(cumulated[-1] / days)以上は必要。 | ||
| - このあたりを初期値として、capacityを適当に固定して脳内シミュレーションする。 | ||
| - capacityを固定すると、各日に載せる積荷の個数は決定論的であり、最終的に`days`日で全て運べるかも決定論的なことに気づいた。 | ||
| - これは、capacityを引数とし、boolを返す関数として実装できる。 | ||
| - capacity_lo <= capacity <= capacity_hiの整数列に対し、上記の関数をkeyとして二分探索したときのfirst Trueを求める問題とみなせる。 | ||
| - 実装 | ||
| - 累積和を前もって計算しておくと、key_function(capacityを引数に、boolを返す関数)の実装がしやすいと感じた。 | ||
| - 計算量 | ||
| - n = weights.length | ||
| - C = capacityの取りうる値の数 とし、 | ||
| - 時間計算量: O(n + logC * days * log n) | ||
| - 空間計算量: O(n) | ||
| - ※range関数は空間的に軽い。 | ||
| - https://docs.python.org/ja/3/library/stdtypes.html#typesseq-range | ||
|
|
||
| ```python3 | ||
| from typing import List | ||
| from itertools import accumulate | ||
| from math import ceil | ||
| import bisect | ||
|
|
||
|
|
||
| class Solution: | ||
| def shipWithinDays(self, weights: List[int], days: int) -> int: | ||
| cumulated = list(accumulate(weights)) | ||
| max_weight = max(weights) | ||
| capacity_lo = max(max_weight, ceil(cumulated[-1] / days)) | ||
| capacity_hi = max_weight * ceil(len(weights) / days) | ||
|
|
||
| def key_function(capacity): | ||
| loaded_total = 0 | ||
| lo = 0 | ||
| for _ in range(days): | ||
| lo = bisect.bisect_left(cumulated, True, lo=lo, key=lambda x: x - loaded_total > capacity) | ||
| if lo == len(cumulated): | ||
| return True | ||
| # 0 < lo <= len(cumulated) | ||
| loaded_total = cumulated[lo - 1] | ||
| return False | ||
|
|
||
| return capacity_lo + bisect.bisect_left(range(capacity_lo, capacity_hi + 1), True, key=key_function) | ||
| ``` | ||
|
|
||
| ここまで27分。 | ||
|
|
||
| - Runtime: 491 ms (Beats 5.04%) | ||
| - memory: 22.76 MB (Beats 16.85%) | ||
| - Runtimeの中央値は200msぐらいだったので、2倍遅い。 | ||
|
|
||
| ## Step 2 | ||
|
|
||
| - レビュー by GPT-5 | ||
| - 関数名key_functionは中身に比して素朴すぎするので、可読性のために改善すべき。is_feasibleとか。 | ||
| - key_functionの計算量が悪い。weightで一重ループを回して貪欲的に区切れる。 | ||
| - 手作業でやってた時は明らかにこう考えていたのに、二分探索に囚われすぎてなぜか無理やりdayごとに二分探索する羽目になった。 | ||
| - 時間計算量: O(logC * days * log n) -> O(logC * n)に改善できる。 | ||
|
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. 上のやり方は自分は考え付かなかったので勉強になりました。
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. ありがとうございます。 確かにそうですね。船が大きく一気に運ぶ前提ならアリかもしれません。 |
||
| - capacity_hiはsum(weights)でいい。 | ||
| - 上限・下限値は2倍冗長でも操作は1回しか増えない。 | ||
|
|
||
| ### 実装2 | ||
|
|
||
| - [実装1](#実装1)の改良。 | ||
| - key関数の計算量を改善。 | ||
| - key関数の命名を改善。 | ||
| - capacityの上限、下限をわかりやすくした。 | ||
| - 時間計算量: O(logC * n) | ||
| - 空間計算量: O(1) <- cumulatedが不要になった | ||
|
|
||
| ```python3 | ||
| from typing import List | ||
| import bisect | ||
|
|
||
|
|
||
| class Solution: | ||
| def shipWithinDays(self, weights: List[int], days: int) -> int: | ||
| def is_feasible(capacity): | ||
| passed_days = 0 | ||
| loaded_today = 0 | ||
| for weight in weights: | ||
| if loaded_today + weight <= capacity: | ||
| loaded_today += weight | ||
| continue | ||
| passed_days += 1 | ||
| loaded_today = weight | ||
| if passed_days == days: | ||
| return False | ||
| return True | ||
|
|
||
| capacity_lo = max(weights) | ||
| capacity_hi = sum(weights) + 1 | ||
| return bisect.bisect_left(range(capacity_hi), True, lo=capacity_lo, key=is_feasible) | ||
| ``` | ||
|
|
||
| Runtime: 175 ms (Beats 77.15%)で、計算量オーダーの改善に従い実際に時短した。 | ||
|
|
||
| - https://github.com/nanae772/leetcode-arai60/pull/43/ | ||
| - 異常入力の検討をされている。 | ||
| - Step 2で色々と参照されていて、追随側としてはありがたい。 | ||
| - [実装2](#実装2)の異常入力に対する耐性 | ||
| - weightが0や負の値であっても、例外は送出しない。 | ||
| - 負の値とかは例えば船から荷物を取り出す操作とかに対応させられそう。(ノーショー) | ||
|
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や負の値を弾くかどうかはユーザーに任せたいと思います。 | ||
| - daysが0や負の値のとき、is_feasibleが常にTrueを返してしまう。 | ||
| - となると、このメソッドはエラーを吐かずに適当な値を出すことになる。 | ||
| - これは、最初にチェックしても良さそう。 | ||
|
|
||
| ## Step 3 | ||
|
|
||
| ### 実装3 | ||
|
|
||
| ```python3 | ||
| from typing import List | ||
| import bisect | ||
|
|
||
|
|
||
| class Solution: | ||
| def shipWithinDays(self, weights: List[int], days: int) -> int: | ||
| if days <= 0: | ||
| raise ValueError(f"days '{days}' must be positive integer") | ||
|
|
||
| def is_feasible(capacity): | ||
| passed_days = 0 | ||
| loaded = 0 | ||
| for weight in weights: | ||
| if loaded + weight <= capacity: | ||
| loaded += weight | ||
| continue | ||
| passed_days += 1 | ||
| if passed_days == days: | ||
| return False | ||
| loaded = weight | ||
|
Comment on lines
+142
to
+144
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. passed_days += 1した直後にチェックしたいという意図も分かるのですが、個人的にはloaded = weightの代入を先に書いた方が少し見やすいかなと思いました。
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. ありがとうございます。
まさに、そこで悩みました。一応時系列を意識して書いてみました。今思いつきましたが、こういうのもいいかも知れません。 if passed_days + 1 == days:
return False
passed_dayes += 1
loaded = weight
これに関してはシンプルさとパフォーマンス最適化のトレードオフですが、個人的にはこの程度ならearly returnする価値が勝ると思いました。 |
||
| return True | ||
|
|
||
| capacity_low = max(weights) | ||
| capacity_high = sum(weights) + 1 | ||
| return bisect.bisect_left( | ||
| range(capacity_high), | ||
| True, | ||
| lo=capacity_low, | ||
| key=is_feasible | ||
| ) | ||
| ``` | ||
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.
is_shippable はいかがでしょうか?
Uh oh!
There was an error while loading. Please reload this page.
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.
ありがとうございます。
shippableも少し検討したんですが、"able"が複数日にわたる輸送計画の可能性ではなく、もっと狭い「
まだ船に積むことができるかある1日において輸送可能かどうか」のニュアンスを持つ気がして、少し違う気がしたのでふんわりとfeasibleにしてみました。