Skip to content
Open
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions .gitignore
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
293 changes: 293 additions & 0 deletions 1011CapacityToShipPackagesWithinDDays.md
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")

Choose a reason for hiding this comment

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

以前こんなコメントを頂いたので参考に貼っておきます。
TORUS0818/leetcode#44 (comment)

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。

一般的に、dead code は避けるものです。

これ刺さりました。

}

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

@Mike0121 Mike0121 Mar 2, 2025

Choose a reason for hiding this comment

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

細かいですが、Shippableですね。
すみません、step2のコメントにありました。

day := 1
weight := 0
for _, w := range weights {
if weight+w > capacity {
day++
weight = 0
}
weight += w

Choose a reason for hiding this comment

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

wもweightだと思うので、名前を変えてもいいかもしれません。
total_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
Copy link

Choose a reason for hiding this comment

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

ここあんまり格好良くないですが、仕方がないですね。

Copy link
Owner Author

Choose a reason for hiding this comment

The 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
}
Copy link

Choose a reason for hiding this comment

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

ヘルパー関数なのと、スコープが短いので好みかもしれませんが、
もう少し変数名をs -> nums, v -> val くらいの長さにしても読みやすいかと思いました。


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しなければ)??
Copy link

Choose a reason for hiding this comment

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

Go では mod 2^32 で同じ値を指すのでそうですね。言語によっては違うことが起きたりしますが。
https://go.dev/ref/spec#Integer_overflow

Copy link
Owner Author

Choose a reason for hiding this comment

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

リンクの共有ありがとうございます。middleを求める方法として、
(1) middle := left + (right-left)/2
(2) middle := int(uint(left+right) >> 1)
の二つの方法があってなんでわかりにくいやり方かつ「mod 2^32 で同じ値を指す」という条件を前提とする方法を使っているのだろうと思ったのですが、割り算を行う(1)よりローコストで計算できるというメリットあるのですね

Copy link

Choose a reason for hiding this comment

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

たとえば、x86 だとシフト命令には、logical (unsigned) と arithmetic (signed) があって、arithmetic は右シフトによってできる空きに最上位の符号ビットが埋められます。cast はコンパイルすればコストはないので一命令でできています。
https://en.wikibooks.org/wiki/X86_Assembly/Shift_and_Rotate
https://docs.oracle.com/cd/E19620-01/805-4693/instructionset-27/index.html

- 以前から気になっていたが、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
}
```