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
173 changes: 173 additions & 0 deletions 1011_capacity_to_ship_packages_within_d_days_medium/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# 問題へのリンク
[Capacity To Ship Packages Within D Days](https://leetcode.com/problems/capacity-to-ship-packages-within-d-days/)

# 言語
Python


# 自分の解法

## step1

```python
class Solution:
def shipWithinDays(self, weights: list[int], days: int) -> int:
if not weights:
return 0

def can_ship_within_days(days: int, capacity: int) -> bool:
cargo_weight = 0
days_required = 1
for weight in weights:
if weight > capacity:
return False
if cargo_weight + weight > capacity:
cargo_weight = 0
days_required += 1
cargo_weight += weight
return days_required <= days

# 0 < capacity <= sum weights
left = 0
right = sum(weights)
while right - left > 1:
mid = (right + left) // 2
if can_ship_within_days(days, mid):
right = mid
else:
left = mid
return right
```

- 単調性あるところに二分探索あり。
- `(left, right]`の範囲で二分探索するパターン。
- `can_ship_within_days`関数内で`weight > capacity`のチェックをしているが、はじめは抜けていた上、なかなか気づきづらい。
- `left`の初期値を`0`にしていたが、`max(weights)`などにすればこのチェックは不要になる。その場合、二分探索も変わる。
- そうはいっても、そもそも`if max(weights) > capacity: return False`とすべき。
- 例えば`weights = [100,]`のとき、can_ship_within_days(2, 50)は`False`を返すべきだが、上記のコードでは`True`を返してしまう。これは2日に分ければ大きな荷物も運べるということになってしまうため。
- `cargo_weight`は複数の荷物をまとめた重さを表す変数としているが、`current_weight`の方がわかりやすいと思った
- `can_ship_within_days`という名前の関数で`days`が引数にないのは違和感があるので、`days`を引数にした。が、`can_ship`にリネームして`days`をキャプチャする形にしたほうが自然だと思った。

`weights`の要素数を`N`、`weights`の要素の和を`S`とすると、
- 時間計算量:`O(N log(S))`
- 空間計算量:`O(N)`

## step2

```python
class Solution:
def shipWithinDays(self, weights: list[int], days: int) -> int:
if not weights:
return 0

def can_ship(capacity: int) -> bool:
current_weight = 0
days_required = 1
for weight in weights:
if current_weight + weight > capacity:
current_weight = 0
days_required += 1
current_weight += weight
return days_required <= days

# max weight <= capacity <= sum weights
left = max(weights) - 1
right = sum(weights)
while right - left > 1:
mid = (right + left) // 2
if can_ship(mid):
right = mid
else:
left = mid
return right
```
- `can_ship_within_days`関数を`can_ship`にリネーム


## step3
```python
class Solution:
def shipWithinDays(self, weights: list[int], days: int) -> int:
# for given capacity, we can check if we can ship all the packages with it
def can_ship(capacity: int) -> bool:
if max(weights) > capacity:
return False

current_weight = 0
days_required = 1
for weight in weights:
current_weight += weight
if current_weight > capacity:
current_weight = weight
days_required += 1
return days_required <= days

left = 0
right = sum(weights)
Comment on lines +105 to +106

Choose a reason for hiding this comment

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

この問題に適した名前にしてもよいのかなと思いました。
以下はやや冗長な名前付けですが例えば

  • left -> max_capacity_cannot_ship
  • right -> min_capacity_can_ship

とかにするとreturnしている変数が求めたいものと一致しているなと分かりやすいかなと思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビューありがとうございます。動的計画法ではさすがに dp等と書くのは避けているのですが、二分探索では left , right を私は基本的に使ってしまっています。が、問題ごとに適切な名前をつけてみるように努力してみます。ご指摘ありがとうございます。

今回の場合、自分なら、rightは常にcan_shipTrueになる、つまり荷物を運ぶのに十分な容量を表し、left は逆に不十分な容量を表すので、

  • left -> insufficient_capacity
  • right -> sufficient_capacity
    などがわかりやすいかなと感じました。
    (二分探索の途中では必ずしもmax/minであるわけではないので、不変条件を表す変数名にしてみました)

while right - left > 1:
Copy link

Choose a reason for hiding this comment

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

読みやすいです。個人的には右側が閉区間で左側が開区間という取り方はあまり見慣れないですが、私の経験が少ないだけかもしれません。

mid = (right + left) // 2
if can_ship(mid):
right = mid
else:
left = mid

return right
```

## step4 (FB)
- 二分探索の `left`/`right`という変数名を問題に適した名前に変える。これで不変条件がわかりやすくなる。
- `left` -> `insufficient_capacity`
- `right` -> `sufficient_capacity`


# 別解・模範解答
`bisect`モジュールを使う方法

```python
import bisect


class Solution:
def shipWithinDays(self, weights: list[int], days: int) -> int:
def can_ship(capacity: int) -> bool:
if max(weights) > capacity:
return False
current_weight = 0
days_required = 1
for weight in weights:
current_weight += weight
if current_weight > capacity:
days_required += 1
current_weight = weight
return days_required <= days

return bisect.bisect_left(range(sum(weights) + 1), True, key=can_ship)
```

- 二分探索を自分で書く前に、まずは`bisect`モジュールを使って通るかを見るのが良い。
- `bisect`モジュールでは`key`引数が使えるので、`can_ship`関数をそのまま渡せる。
- ソート済みのリストを用意する必要があるが、`range`で用意できる。`bool`の配列ではソートすると`False`が先に来るので、`True`が初めて出現するインデックスを求めることになる。
- `bisect`に渡せる配列は`SupportsLenAndGetItem`プロトコルを満たしていれば良いので、`__len__`と`__getitem__`を実装したクラスならば良い。

Choose a reason for hiding this comment

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

このあたり勉強になりました!


```python
import bisect

class MyRange:
def __init__(self):
self.length = 5

def __len__(self):
return self.length

def __getitem__(self, key: int) -> int:
return 2 * key


g = MyRange() # [0, 2, 4, 6, 8]
bisect.bisect_left(g, 3) # 2
```


# 次に解く問題の予告
- [Unique Paths II](https://leetcode.com/problems/unique-paths-ii/)
- [Longest Increasing Subsequence](https://leetcode.com/problems/longest-increasing-subsequence/)
37 changes: 37 additions & 0 deletions 1011_capacity_to_ship_packages_within_d_days_medium/step1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#
# @lc app=leetcode id=1011 lang=python3
#
# [1011] Capacity To Ship Packages Within D Days
#

# @lc code=start
class Solution:
def shipWithinDays(self, weights: list[int], days: int) -> int:
if not weights:
return 0

def can_ship_within_days(days: int, capacity: int) -> bool:
cargo_weight = 0
days_required = 1
for weight in weights:
if weight > capacity:
return False
if cargo_weight + weight > capacity:
cargo_weight = 0
days_required += 1
cargo_weight += weight
return days_required <= days

# 0 < capacity <= sum weights
left = 0
right = sum(weights)
while right - left > 1:
mid = (right + left) // 2
if can_ship_within_days(days, mid):
right = mid
else:
left = mid
return right


# @lc code=end
37 changes: 37 additions & 0 deletions 1011_capacity_to_ship_packages_within_d_days_medium/step2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#
# @lc app=leetcode id=1011 lang=python3
#
# [1011] Capacity To Ship Packages Within D Days
#

# @lc code=start
class Solution:
def shipWithinDays(self, weights: list[int], days: int) -> int:
if not weights:
return 0

def can_ship(capacity: int) -> bool:
if max(weights) > capacity:
return False
current_weight = 0
days_required = 1
for weight in weights:
if current_weight + weight > capacity:
current_weight = 0
days_required += 1
current_weight += weight
return days_required <= days

# max weight <= capacity <= sum weights
left = 0
right = sum(weights)
while right - left > 1:
mid = (right + left) // 2
if can_ship(mid):
right = mid
else:
left = mid
return right


# @lc code=end
36 changes: 36 additions & 0 deletions 1011_capacity_to_ship_packages_within_d_days_medium/step3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#
# @lc app=leetcode id=1011 lang=python3
#
# [1011] Capacity To Ship Packages Within D Days
#

# @lc code=start
class Solution:
def shipWithinDays(self, weights: list[int], days: int) -> int:
# for given capacity, we can check if we can ship all the packages with it
def can_ship(capacity: int) -> bool:
if max(weights) > capacity:
return False

current_weight = 0
days_required = 1
for weight in weights:
current_weight += weight
if current_weight > capacity:
current_weight = weight
days_required += 1
return days_required <= days

left = 0
right = sum(weights)
while right - left > 1:
mid = (right + left) // 2
if can_ship(mid):
right = mid
else:
left = mid

return right


# @lc code=end
29 changes: 29 additions & 0 deletions 1011_capacity_to_ship_packages_within_d_days_medium/use_bisect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#
# @lc app=leetcode id=1011 lang=python3
#
# [1011] Capacity To Ship Packages Within D Days
#

# @lc code=start

import bisect


class Solution:
def shipWithinDays(self, weights: list[int], days: int) -> int:
def can_ship(capacity: int) -> bool:
if max(weights) > capacity:
return False
current_weight = 0
days_required = 1
for weight in weights:
current_weight += weight
if current_weight > capacity:
days_required += 1
current_weight = weight
return days_required <= days

return bisect.bisect_left(range(sum(weights) + 1), True, key=can_ship)


# @lc code=end