-
Notifications
You must be signed in to change notification settings - Fork 0
1011. Capacity To Ship Packages Within D Days #42
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,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,293 @@ | ||
| 問題: https://leetcode.com/problems/capacity-to-ship-packages-within-d-days/description/ | ||
|
|
||
| ### Step 1 | ||
| - 見当もつかなかったので他の人のプルリクを見た | ||
| - https://github.com/hayashi-ay/leetcode/pull/55/files#diff-4e146417f14c744a10f851601f26cd2cb17b420ff966720e568f6f5679aa475eR1 | ||
| - https://github.com/Mike0121/LeetCode/pull/46/files | ||
| - なるほど、答えのcapacityは[weightsの最大要素, weightsの総計]の区間に存在することになるので、その範囲を探索すれば良いのか | ||
| - まずは線形に探索していく。時間計算量はO(len(weights) * (weightsSum-maxWeight))になるので、weightsの要素が大きいと時間がかかりそう | ||
| - 案の定TLEした | ||
|
|
||
| ```Go | ||
| func shipWithinDays(weights []int, days int) int { | ||
| maxWeight := slices.Max(weights) | ||
| weightSum := 0 | ||
| for _, w := range weights { | ||
| weightSum += w | ||
| } | ||
| for capacity := maxWeight; capacity <= weightSum; capacity++ { | ||
| if isShipableWithinCapacity(weights, days, capacity) { | ||
| return capacity | ||
| } | ||
| } | ||
| panic("unreacheable") | ||
| } | ||
|
|
||
| func isShipableWithinCapacity(weights []int, days int, capacity int) bool { | ||
| day := 1 | ||
| weight := 0 | ||
| for _, w := range weights { | ||
| if weight+w > capacity { | ||
| day++ | ||
| weight = 0 | ||
| } | ||
| weight += w | ||
| if day > days { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
| ``` | ||
|
|
||
| - ここで登場するのが二分探索 | ||
| - [weightsの最大要素, weightsの総計]の区間を、 | ||
| true: 与えられたdays以内に出荷できるcapacity と定義すると、 | ||
| [false,...,false,true,...,true]という配列になり、一番左のtrueの位置を求める | ||
| - trueは必ず存在するので、閉区間を使って範囲を狭めていき、left==rightとなったらその値が求めたいcapacity | ||
| - Goではcapはbuilt-in関数名になっているため、変数名として使用することは避ける | ||
| - Goにはスライスの要素の総計を求める標準ライブラリ関数が存在しない | ||
|
|
||
| ```Go | ||
| func shipWithinDays(weights []int, days int) int { | ||
| maxWeight := slices.Max(weights) | ||
| weightsSum := 0 | ||
| for _, w := range weights { | ||
| weightsSum += w | ||
| } | ||
| low := maxWeight | ||
| high := weightsSum | ||
| for low < high { | ||
| middle := low + (high-low)/2 | ||
| if isShipableWithinCapacity(weights, days, middle) { | ||
| high = middle | ||
| } else { | ||
| low = middle + 1 | ||
| } | ||
| } | ||
| return low | ||
| } | ||
|
|
||
| func isShipableWithinCapacity(weights []int, days int, capacity int) bool { | ||
|
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.
|
||
| day := 1 | ||
| weight := 0 | ||
| for _, w := range weights { | ||
| if weight+w > capacity { | ||
| day++ | ||
| weight = 0 | ||
| } | ||
| weight += w | ||
|
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. wもweightだと思うので、名前を変えてもいいかもしれません。 |
||
| if day > days { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 2 | ||
| #### 2a | ||
| - shipableではなくshippable | ||
| - ヘルパー関数をshippableかどうかのbool値で出力する方法以外に、 | ||
| capacityに対して何日必要かを返してそれがdays以下かどうかを呼び出し元で確認する方法もある。 | ||
| このほうがヘルパー関数がシンプルになる一方、daysを超えた時点でfalseを返す方が速い | ||
|
|
||
| ```Go | ||
| func shipWithinDays(weights []int, days int) int { | ||
| maxWeight := slices.Max(weights) | ||
| weightsSum := 0 | ||
| for _, w := range weights { | ||
| weightsSum += w | ||
| } | ||
| low := maxWeight | ||
| high := weightsSum | ||
| for low < high { | ||
| middle := low + (high-low)/2 | ||
| requiredDays := requiredDaysToShip(weights, middle) | ||
| if requiredDays <= days { | ||
| high = middle | ||
| } else { | ||
| low = middle + 1 | ||
| } | ||
| } | ||
| return low | ||
| } | ||
|
|
||
| // requiredDaysToShip returns the minimum days to ship within the given capacity. | ||
| func requiredDaysToShip(weights []int, capacity int) int { | ||
| days := 1 | ||
| weight := 0 | ||
| for _, w := range weights { | ||
| if weight+w > capacity { | ||
| days++ | ||
| weight = w | ||
| continue | ||
| } | ||
| weight += w | ||
| } | ||
| return days | ||
| } | ||
| ``` | ||
|
|
||
| #### 2b | ||
| - 標準ライブラリ関数のslices.BinarySearchFuncを使ってみる | ||
| - [maxWeight,weightsSum]区間のスライスを作成しないといけないため、 | ||
| 空間計算量がO(n)になる | ||
| - pythonのbisect_leftならrangeを使って空間計算量をO(1)に抑えられるらしい | ||
| - https://github.com/fhiyo/leetcode/pull/45#discussion_r1682470839 | ||
| - shippableなcapacityを1, そうでないものを0として | ||
| [0,...,0,1,...,1]という配列の左端の1の位置を返す | ||
| - bool値を使わなかったのは、BinarySearchFuncの第三引数がint型を返す関数であり、bool値同士の演算をint型に変換するのが面倒だったから | ||
|
|
||
| ```Go | ||
| func SumInt(s []int) int { | ||
| sum := 0 | ||
| for _, v := range s { | ||
| sum += v | ||
| } | ||
| return sum | ||
| } | ||
|
|
||
| func shipWithinDays(weights []int, days int) int { | ||
| maxWeight := slices.Max(weights) | ||
| weightsSum := SumInt(weights) | ||
| capacities := make([]int, weightsSum-maxWeight+1) | ||
| for i := range capacities { | ||
| capacities[i] = maxWeight + i | ||
| } | ||
|
Comment on lines
+154
to
+157
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. pythonだとrangeで一発ですね |
||
| index, _ := slices.BinarySearchFunc(capacities, 1, func(capacity int, t int) int { | ||
| return isShippable(weights, days, capacity) - t | ||
| }) | ||
| return capacities[index] | ||
| } | ||
|
|
||
| // isShippable returns 1 if shippable and 0 if not | ||
| func isShippable(weights []int, days int, capacity int) int { | ||
| day := 1 | ||
| weight := 0 | ||
| for _, w := range weights { | ||
| if weight+w > capacity { | ||
| day++ | ||
| weight = 0 | ||
| } | ||
| weight += w | ||
| if day > days { | ||
| return 0 | ||
| } | ||
| } | ||
| return 1 | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 3 | ||
| - 日数を調べるよりヘルパー関数で判定できた方が自分は好きなのでstep1の方法 | ||
| - `isShippable`より`canShip`の方がシンプル | ||
| - https://github.com/goto-untrapped/Arai60/pull/41/files#diff-cc45bac68955c702274e070386dd9a6db7bad032fdc75cf32cbea4781f618685R17 | ||
| - `low := slices.Max(weights)`とすることに抵抗があったが、割とそうしている人がいる | ||
| - https://github.com/fhiyo/leetcode/pull/45/files#diff-3e42d068b82e2a1be434dc989edc077d304c433f9a25ad4a2b3bc8f9223e43bcR33 | ||
| - `day` -> `daysRequired` | ||
| - https://github.com/fhiyo/leetcode/pull/45/files#r1682612140 | ||
| - ヘルパー関数の`canShip()`をinner functionにするかどうか迷った。 | ||
| - inner functionにするメリットは、依存関係が生じるリスクをなくせること。 | ||
| 引数が減ること | ||
| - Goのスライスは参照渡しなのでメモリ使用量が増える心配はしなくてよい | ||
| - inner functionではなく、外で定義した方が個人的には見やすい | ||
| - 機能面的にはinner functionにした方がメリットが多そう | ||
| - weightsが空の時、slices.Maxでpanicする | ||
|
|
||
| ```Go | ||
| func SumInts(s []int) int { | ||
| sum := 0 | ||
| for _, v := range s { | ||
| sum += v | ||
| } | ||
| return sum | ||
| } | ||
|
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. ヘルパー関数なのと、スコープが短いので好みかもしれませんが、 |
||
|
|
||
| func shipWithinDays(weights []int, days int) int { | ||
| canShip := func(capacity int) bool { | ||
| daysRequired := 1 | ||
| loadedWeight := 0 | ||
| for _, weight := range weights { | ||
| if loadedWeight+weight > capacity { | ||
| daysRequired++ | ||
| loadedWeight = 0 | ||
| } | ||
| if daysRequired > days { | ||
| return false | ||
| } | ||
| loadedWeight += weight | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| low := slices.Max(weights) | ||
| high := SumInts(weights) | ||
| for low < high { | ||
| middle := low + (high-low)/2 | ||
| if canShip(middle) { | ||
| high = middle | ||
| } else { | ||
| low = middle + 1 | ||
| } | ||
| } | ||
| return low | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 4 | ||
| - slices.BinarySearchFuncの内部実装を見る | ||
| - ⚠️質問 | ||
| - https://cs.opensource.google/go/go/+/master:src/slices/sort.go;l=158 | ||
| - middleを計算する際にoverflow対策のため、 | ||
| `middle := int(uint(low+high) >> 1)`としている | ||
| - これがoverflow対策になっているのは、int型よりuint型の方が大きい正の数を表現できるから?? | ||
| - すなわち、32bitマシンにおいてint型はint32と同じ大きさで、 | ||
| 同様にuint型はuint32と同じになり、 | ||
| 表現できる最大値がそれぞれ2^31-1と2^32-1で後者の方が大きいからuint(low+high)でoverflowしない | ||
| - 計算結果は`(low+high) / 2`と同じ(overflowしなければ)?? | ||
|
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 では mod 2^32 で同じ値を指すのでそうですね。言語によっては違うことが起きたりしますが。
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. リンクの共有ありがとうございます。middleを求める方法として、 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. たとえば、x86 だとシフト命令には、logical (unsigned) と arithmetic (signed) があって、arithmetic は右シフトによってできる空きに最上位の符号ビットが埋められます。cast はコンパイルすればコストはないので一命令でできています。 |
||
| - 以前から気になっていたが、Goの標準ライブラリの内部実装を見ると、 | ||
| 一文字変数や省略形が多用されている印象を受ける | ||
| - 今回も、i -> low, j -> high, h -> middle としたいところ | ||
|
|
||
| ```Go | ||
| func BinarySearchFunc[S ~[]E, E, T any](x S, target T, cmp func(E, T) int) (int, bool) { | ||
| n := len(x) | ||
| i, j := 0, n | ||
| for i < j { | ||
| h := int(uint(i+j) >> 1) | ||
| if cmp(x[h], target) < 0 { | ||
| i = h + 1 | ||
| } else { | ||
| j = h | ||
| } | ||
| } | ||
| return i, i < n && cmp(x[i], target) == 0 | ||
| } | ||
| ``` | ||
|
|
||
| - slices.BinarySearchもやってみる | ||
| - isNaN関数の実装がいまいちよくわからなかった | ||
| - math.IsNaNの説明に書いてあった | ||
| - https://cs.opensource.google/go/go/+/refs/tags/go1.24.0:src/math/bits.go;l=35 | ||
| - IEEE754でNaNだけx!=xになると定義されているのか | ||
|
|
||
| ```Go | ||
| func BinarySearch[S ~[]E, E cmp.Ordered](x S, target E) (int, bool) { | ||
| n := len(x) | ||
| i, j := 0, n | ||
| for i < j { | ||
| h := int(uint(i+j) >> 1) | ||
| if cmp.Less(x[h], target) { | ||
| i = h + 1 | ||
| } else { | ||
| j = h | ||
| } | ||
| } | ||
| return i, i < n && (x[i] == target || (isNaN(x[i]) && isNaN(target))) | ||
| } | ||
|
|
||
| func isNaN[T cmp.Ordered](x T) bool { | ||
| return x != x | ||
| } | ||
| ``` | ||
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.
以前こんなコメントを頂いたので参考に貼っておきます。
TORUS0818/leetcode#44 (comment)
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.
ありがとうございます。
これ刺さりました。