Skip to content
Open
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
144 changes: 144 additions & 0 deletions Python3/253. Meeting Rooms II.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
## Step 1. Initial Solution

- 前問と同じような構造
- 誰かがやっていた累積和の方法だとやりやすい?
- メモリを時間分確保するやり方だったか?それだと効率はあまりよくなさそう
- 他に良いやり方を思い出せなかったのでheapを使った処理を元に考える
- 終了時刻を保持しながら次のスタート時間を見ていけば重複数の最大が分かる

```python
"""
Definition of Interval:
class Interval(object):
def __init__(self, start, end):
self.start = start
self.end = end
"""

def interval_lt(self, other) -> bool:
if self.start < other.start:
return True
return False

Interval.__lt__ = interval_lt

class Solution:
def minMeetingRooms(self, intervals: List[Interval]) -> int:
heapq.heapify(intervals)
Comment on lines +18 to +27

Choose a reason for hiding this comment

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

確かにこのように書けますが、可読性・保守性いずれの観点からも個人的には避けたいと感じました。

以下のようなナイーブな書き方で十分かなと感じます。

        remaining_intervals = [
            (interval.start, interval.end) for interval in intervals
        ]
        heapq.heapify(remaining_intervals)

required_days = 0
sorted_end_times = []
while intervals:
interval = heapq.heappop(intervals)
while sorted_end_times and interval.start >= sorted_end_times[-1]:
sorted_end_times.pop()
sorted_end_times.append(interval.end)
sorted_end_times.sort(reverse=True)
required_days = max(required_days, len(sorted_end_times))
return required_days
```

### Complexity Analysis

- 時間計算量:O(n^2 log n)
- n回のpopの中に最大nlognのソートが入る
- n < 500なので最大250000*9=2.25*10^6 → 10^8 Steps/s なら 50ms程度
- 空間計算量:O(n)

## Step 2. Alternatives

- AppendからのSortは効率が悪い
- bisect_leftからのinsertの方が良い?
- 以下だとend_indexの位置が間違って取得される
- 数十分かけてもよく分からなかったので一旦置いておく

```python
end_index = bisect.bisect(sorted_end_times, interval.end, key=lambda x: -x)
sorted_end_times.insert(end_index, interval.end)
```

- insortでも良い
- こちらはちゃんと動いた

```python
bisect.insort(sorted_end_times, interval.end, key=lambda x: -x)
```

- ドキュメントをよく読むと以下のような記述があった
- insort
- `To support inserting records in a table, the *key* function (if any) is applied to *x* for the search step but not for the insertion step.`
- bisect_left
- `To support searching complex records, the key function is not applied to the *x* value.`
- なるほど、検索時にkeyがxに適用されるかされないかの違いがあったのか
- insortを使わない場合は昇順でpop(0)を許容するか、-1倍した値を保持しておくか

```python
while sorted_end_times and interval.start >= sorted_end_times[0]:
sorted_end_times.pop(0)
end_index = bisect.bisect(sorted_end_times, interval.end)
sorted_end_times.insert(end_index, interval.end)
```

```python
while sorted_end_times and interval.start >= -sorted_end_times[-1]:
sorted_end_times.pop()
end_index = bisect.bisect(sorted_end_times, -interval.end)
sorted_end_times.insert(end_index, -interval.end)
```

- https://github.com/tokuhirat/LeetCode/pull/56/files#diff-e176b724e8fbbafc36b447606ee37b1d5b91eca27193941d2c6e233c3c1c28c7R4
- 累積和はやはりメモリを時間分確保するやり方か
- シンプルだが効率は悪い
- その後で座標圧縮をしているが少し理解が難しかった
- List[List[int]]の展開にsum(intervals, [])を使っているのは初めて見た
- https://github.com/olsen-blue/Arai60/pull/57/files#r2030075157
- このやり方は確かにありか
- 自分はそもそも累積和を考える時にdict型を使えば良いのでは?と思った

```python
class Solution:
def minMeetingRooms(self, intervals: List[Interval]) -> int:
meeting_num_change = defaultdict(int)
for interval in intervals:
meeting_num_change[interval.start] += 1
meeting_num_change[interval.end] -= 1

required_days = 0
num_meetings = 0
for change_time in sorted(meeting_num_change.keys()):
num_meetings += meeting_num_change[change_time]
required_days = max(required_days, num_meetings)
return required_days
```

- [https://github.com/shining-ai/leetcode/blob/main/arai60/54-60_others/56_253_Meeting Rooms II/level_5.py](https://github.com/shining-ai/leetcode/blob/main/arai60/54-60_others/56_253_Meeting%20Rooms%20II/level_5.py)
- 終了時間だけを管理しておく方法
- 新しく始める時に前のミーティングが終わっていたら使うみたいなイメージのよう
- このイメージだとミーティング数を数えるのは最後のstartの後だけで済むのか

## Step 3. Final Solution

- 元の実装の改良版で落ち着いた

```python
import bisect

def interval_lt(self, other):
if self.start < other.start:
return True
return False

Interval.__lt__ = interval_lt

class Solution:
def minMeetingRooms(self, intervals: List[Interval]) -> int:
heapq.heapify(intervals)
end_times = []
required_days = 0
while intervals:
interval = heapq.heappop(intervals)
while end_times and end_times[-1] <= interval.start:
end_times.pop()
bisect.insort(end_times, interval.end, key=lambda x: -x)
Comment on lines +139 to +141

Choose a reason for hiding this comment

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

実はend_timesの方もheapで十分ですね。

Choose a reason for hiding this comment

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

import heapq


class Solution:
    def minMeetingRooms(self, intervals: List[Interval]) -> int:
        remaining_intervals = [
            (interval.start, interval.end) for interval in intervals
        ]
        heapq.heapify(remaining_intervals)
        holding_end_times = []
        required_days = 0
        while remaining_intervals:
            next_start, next_end = heapq.heappop(remaining_intervals)
            while holding_end_times and next_start >= holding_end_times[0]:
                heapq.heappop(holding_end_times)
            heapq.heappush(holding_end_times, next_end)
            required_days = max(required_days, len(holding_end_times))
        return required_days

Choose a reason for hiding this comment

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

一重ループで書くとこうなります。

import heapq


class Solution:
    def minMeetingRooms(self, intervals: list[Interval]) -> int:
        start_times = [interval.start for interval in intervals]
        end_times = [interval.end for interval in intervals]
        heapq.heapify(start_times)
        heapq.heapify(end_times)
        required_days = 0
        max_required_days = 0
        while start_times and end_times:
            if start_times[0] < end_times[0]:
                heapq.heappop(start_times)
                required_days += 1
                max_required_days = max(max_required_days, required_days)
            else:
                heapq.heappop(end_times)
                required_days -= 1
        return max_required_days

Choose a reason for hiding this comment

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

上のコードをGemini 3 Proにレビューしてもらったら、

Pythonの sort() (Timsort) は非常に高速に最適化されています。全ての要素を出し入れする場合は、heapq を使うよりも、最初に sort() して for ループやポインタで走査する方がシンプルで高速な場合が多いです。

とのことでした。これはそうかもしれません。

時間計算量はheapifyはO(n)ですが、ループ内でのheappopがn回行われるため全体O(n log n)で、実質ソートと同じコストがかかり、定数倍の勝負です。これは綿密にチューニングされたsort()に平均的に勝てる気がしないですね。

required_days = max(required_days, len(end_times))
return required_days
```