Skip to content

Commit 068cf43

Browse files
committed
Add LeetCode 1204. Last Person to Fit in the Bus (SQL/Pandas) by Claude Sonnet 4.6 Extended
1 parent a354e0f commit 068cf43

5 files changed

Lines changed: 4120 additions & 4 deletions

File tree

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
# Pandas 2.2.2
2+
3+
## 0) 前提
4+
5+
- 環境: **Python 3.10.15 / pandas 2.2.2**
6+
- 指定シグネチャ厳守(関数名・引数名・返却列・順序)
7+
- I/O 禁止、不要な `print``sort_values` 禁止
8+
9+
---
10+
11+
## 1) 問題
12+
13+
- バスの重量制限 **1000 kg** を超えない範囲で乗車できる **最後の人物名** を返す
14+
- 入力 DF: `queue(person_id, person_name, weight, turn)`
15+
- 出力: `person_name`(1行)— `turn` 昇順で累積体重が 1000 以下となる最大 `turn` の人
16+
17+
---
18+
19+
## 2) 実装(指定シグネチャ厳守)
20+
21+
```python
22+
# Analyze Complexity
23+
# Runtime 311 ms
24+
# Beats 83.46%
25+
# Memory 67.38 MB
26+
# Beats 80.11%
27+
28+
import pandas as pd
29+
30+
def last_passenger(queue: pd.DataFrame) -> pd.DataFrame:
31+
"""
32+
Returns:
33+
pd.DataFrame: 列名と順序は ['person_name']
34+
"""
35+
# Step1: turn 昇順で cumulative weight を計算
36+
# sort_values は結果の正確性のために必要(出力用ではなく計算用)
37+
cum_w = (
38+
queue
39+
.sort_values('turn') # 乗車順に並べる(計算用)
40+
['weight']
41+
.cumsum() # 累積和 O(N)
42+
)
43+
44+
# Step2: 累積体重が 1000 以下の行マスクを生成
45+
mask = cum_w.le(1000) # le = <=
46+
47+
# Step3: 条件を満たす最後の行(最大 turn)を idxmax で取得
48+
# cum_w は turn 昇順なので、最後の True のインデックス = 答え
49+
last_idx = mask[mask].index[-1] # O(N)
50+
51+
# Step4: 仕様列のみ返却
52+
return pd.DataFrame({'person_name': [queue.at[last_idx, 'person_name']]})
53+
```
54+
55+
---
56+
57+
### 別解(`loc` + `tail` チェーン版)
58+
59+
```python
60+
# Analyze Complexity
61+
# Runtime 320 ms
62+
# Beats 66.54%
63+
# Memory 67.58 MB
64+
# Beats 57.25%
65+
66+
import pandas as pd
67+
68+
def last_passenger(queue: pd.DataFrame) -> pd.DataFrame:
69+
"""
70+
Returns:
71+
pd.DataFrame: 列名と順序は ['person_name']
72+
"""
73+
return (
74+
queue
75+
.sort_values('turn') # 計算用ソート
76+
.assign(cum_w=lambda df: df['weight'].cumsum())
77+
.loc[lambda df: df['cum_w'].le(1000), ['person_name']]
78+
.tail(1) # 最後の1行 = 答え
79+
.reset_index(drop=True) # インデックスリセット
80+
)
81+
```
82+
83+
---
84+
85+
## 3) アルゴリズム説明
86+
87+
**使用 API**:
88+
89+
- `sort_values('turn')` — 乗車順に整列(計算の前処理として必須)
90+
- `Series.cumsum()` — 体重の累積和を O(N) で計算
91+
- `Series.le(1000)` — 要素ごとの `<=` 比較、Boolean マスク生成
92+
- `mask[mask].index[-1]` / `tail(1)` — 条件を満たす最後の行を軽量に抽出
93+
- `reset_index(drop=True)` — 出力インデックスを 0 始まりに正規化
94+
95+
**NULL / 重複 / 型の考慮**:
96+
97+
| 考慮点 | 対応 |
98+
| ---------------------- | ------------------------------------------------------------------- |
99+
| `turn` に重複なし | 問題定義で保証済み(1〜n のユニーク値) |
100+
| `weight` の NULL | 問題定義で保証済み、ただし実運用では `fillna(0)` を検討 |
101+
| `cumsum` の型 | `int``int64` に自動昇格、オーバーフロー不要(最大 1000 kg 前後) |
102+
| 返却 DF のインデックス | `reset_index(drop=True)` で 0 始まりに統一 |
103+
104+
---
105+
106+
## 4) 計算量(概算)
107+
108+
| 処理 | 計算量 | 備考 |
109+
| ----------------------- | -------------- | -------------------- |
110+
| `sort_values('turn')` | **O(N log N)** | ボトルネック |
111+
| `cumsum()` | **O(N)** | 線形スキャン |
112+
| `le(1000)` | **O(N)** | 要素比較 |
113+
| `index[-1]` / `tail(1)` | **O(1)** | インデックスアクセス |
114+
| 全体 | **O(N log N)** | ソートが支配的 |
115+
116+
> `turn` が既にソート済みで投入される場合は **O(N)** に短縮可能。
117+
118+
---
119+
120+
## 5) 図解(Mermaid)
121+
122+
```mermaid
123+
flowchart TD
124+
A["入力: queue DataFrame\nperson_id, person_name, weight, turn"]
125+
B["sort_values 'turn'\n乗車順に整列\n計算用ソート O(N log N)"]
126+
C["cumsum\nweight を累積加算\ncum_w 列を生成 O(N)"]
127+
D["le 1000\ncum_w <= 1000 の Boolean マスク生成 O(N)"]
128+
E["tail(1) または index[-1]\n条件を満たす最後の行を抽出 O(1)"]
129+
F["reset_index\nインデックス正規化"]
130+
G["出力: person_name\n例: John Cena"]
131+
132+
A --> B
133+
B --> C
134+
C --> D
135+
D --> E
136+
E --> F
137+
F --> G
138+
139+
style C fill:#d4edda,stroke:#28a745
140+
style D fill:#d4edda,stroke:#28a745
141+
style E fill:#cce5ff,stroke:#004085
142+
```
143+
144+
---
145+
146+
### 動作トレース(例題データ)
147+
148+
```
149+
入力(sort_values後):
150+
turn │ person_name │ weight │ cum_w │ mask
151+
──────┼─────────────┼────────┼───────┼──────
152+
1 │ Alice │ 250 │ 250 │ True
153+
2 │ Alex │ 350 │ 600 │ True
154+
3 │ John Cena │ 400 │ 1000 │ True ← tail(1) で取得
155+
4 │ Marie │ 200 │ 1200 │ False
156+
5 │ Bob │ 175 │ 1375 │ False
157+
6 │ Winston │ 500 │ 1875 │ False
158+
159+
出力:
160+
person_name
161+
─────────────
162+
John Cena ✅
163+
```
164+
165+
## パフォーマンス改善分析
166+
167+
## 現状のボトルネット診断
168+
169+
```
170+
現在の処理フローとコスト:
171+
172+
sort_values('turn') O(N log N) ← pandas オーバーヘッド大
173+
174+
cumsum() O(N) ← pandas Series 処理
175+
176+
le(1000) → tail(1) O(N) ← 全行スキャン
177+
🔴 1000以下の最後を線形探索
178+
```
179+
180+
**2つの改善ポイント**:
181+
182+
1. `pandas` の内部オーバーヘッドを `numpy` で削減
183+
2. `le(1000).tail(1)`**線形探索**`np.searchsorted`**二分探索 O(log N)** に変換
184+
185+
---
186+
187+
## 改善の核心:`searchsorted` が使える理由
188+
189+
```
190+
全 weight > 0 が保証されている
191+
192+
cumsum は単調増加が確定
193+
194+
二分探索(searchsorted)が適用可能!
195+
196+
[250, 600, 1000, 1200, 1375, 1875]
197+
198+
searchsorted(1000, side='right') = 3
199+
→ index 3-1 = 2 が答え(John Cena)
200+
201+
線形探索 O(N) → 二分探索 O(log N) に短縮
202+
```
203+
204+
---
205+
206+
## 改善案①:numpy 完全移行(推奨)
207+
208+
```python
209+
# Analyze Complexity
210+
# Runtime 295 ms
211+
# Beats 96.65%
212+
# Memory 66.89 MB
213+
# Beats 98.51%
214+
215+
import pandas as pd
216+
import numpy as np
217+
218+
def last_passenger(queue: pd.DataFrame) -> pd.DataFrame:
219+
"""
220+
Returns:
221+
pd.DataFrame: 列名と順序は ['person_name']
222+
"""
223+
# numpy 配列に一括変換(pandas オーバーヘッド排除)
224+
turns = queue['turn'].to_numpy() # int64
225+
weights = queue['weight'].to_numpy() # int64
226+
names = queue['person_name'].to_numpy() # object
227+
228+
# argsort で turn 昇順のインデックス配列を取得
229+
order = np.argsort(turns) # O(N log N)
230+
231+
# 重みを turn 順に並べて累積和
232+
cum_w = weights[order].cumsum() # O(N)
233+
234+
# 🔑 searchsorted: 単調増加列への二分探索 O(log N)
235+
# side='right': 1000 より大きくなる最初の位置を返す → -1 で最後の有効位置
236+
last_pos = np.searchsorted(cum_w, 1000, side='right') - 1
237+
238+
return pd.DataFrame(
239+
{'person_name': [names[order[last_pos]]]}
240+
)
241+
```
242+
243+
---
244+
245+
## 改善案②:`turn` を直接インデックスに利用(ソート省略)
246+
247+
```python
248+
# Analyze Complexity
249+
# Runtime 287 ms
250+
# Beats 98.88%
251+
# Memory 66.83 MB
252+
# Beats 98.51%
253+
254+
import pandas as pd
255+
import numpy as np
256+
257+
def last_passenger(queue: pd.DataFrame) -> pd.DataFrame:
258+
"""
259+
turn は 1〜N の連続整数が保証されている
260+
→ argsort 不要、直接配置でソート相当が O(N) で完結
261+
Returns:
262+
pd.DataFrame: 列名と順序は ['person_name']
263+
"""
264+
n = len(queue)
265+
266+
# turn(1-indexed) をそのまま位置として使う O(N)
267+
weights_sorted = np.empty(n, dtype=np.int64)
268+
names_sorted = np.empty(n, dtype=object)
269+
270+
turns = queue['turn'].to_numpy() - 1 # 0-indexed に変換
271+
weights = queue['weight'].to_numpy()
272+
names = queue['person_name'].to_numpy()
273+
274+
weights_sorted[turns] = weights # 直接配置
275+
names_sorted[turns] = names
276+
277+
cum_w = weights_sorted.cumsum() # O(N)
278+
last_pos = np.searchsorted(cum_w, 1000, side='right') - 1
279+
280+
return pd.DataFrame(
281+
{'person_name': [names_sorted[last_pos]]}
282+
)
283+
```
284+
285+
---
286+
287+
## `searchsorted` 動作トレース
288+
289+
```
290+
cum_w(turn昇順):
291+
index: 0 1 2 3 4 5
292+
value: [250, 600, 1000, 1200, 1375, 1875]
293+
294+
np.searchsorted(cum_w, 1000, side='right')
295+
296+
side='right':
297+
1000 と等しい値の「右側」= index 3 を返す
298+
299+
last_pos = 3 - 1 = 2
300+
301+
names_sorted[2] = 'John Cena' ✅
302+
```
303+
304+
---
305+
306+
## 全手法パフォーマンス比較
307+
308+
| 手法 | ソート | 検索 | メモリ | 推定 Beats |
309+
| -------------------------------------- | ----------------- | ------------ | -------------------- | ---------- |
310+
| 元の実装(`cumsum + tail`| O(N log N) pandas | O(N) 線形 | pandas Series × 複数 | ~83% |
311+
| 改善①(numpy + `searchsorted`| O(N log N) numpy | **O(log N)** | numpy配列のみ | **~90%↑** |
312+
| **改善②(直接配置 + `searchsorted`** | **O(N) 配置** | **O(log N)** | numpy配列のみ | **~95%↑** |
313+
314+
---
315+
316+
## 図解(Mermaid)
317+
318+
```mermaid
319+
flowchart TD
320+
A["入力: queue DataFrame\nperson_id, person_name, weight, turn"]
321+
322+
subgraph old ["❌ 旧実装"]
323+
B1["sort_values pandas\nO(N log N) + オーバーヘッド"]
324+
B2["cumsum + le(1000) + tail\nO(N) 線形探索"]
325+
B1 --> B2
326+
end
327+
328+
subgraph new1 ["✅ 改善①: numpy移行"]
329+
C1["to_numpy + argsort\nO(N log N) 軽量"]
330+
C2["cumsum + searchsorted\nO(N) + O(log N)"]
331+
C1 --> C2
332+
end
333+
334+
subgraph new2 ["🚀 改善②: 直接配置"]
335+
D1["turn-1 を index として直接配置\nO(N) ソート相当"]
336+
D2["cumsum + searchsorted\nO(N) + O(log N)"]
337+
D1 --> D2
338+
end
339+
340+
A --> old
341+
A --> new1
342+
A --> new2
343+
344+
style D1 fill:#d4edda,stroke:#28a745
345+
style D2 fill:#d4edda,stroke:#28a745
346+
style C1 fill:#cce5ff,stroke:#004085
347+
style C2 fill:#cce5ff,stroke:#004085
348+
```
349+
350+
---
351+
352+
**改善のポイントまとめ**:
353+
354+
`turn`**1〜N の連続整数** であるという制約を最大活用し、`argsort` を配列直接配置 O(N) に置き換え、かつ単調増加の `cumsum` に対して `searchsorted` で二分探索を適用することが最大の改善ポイントです。

0 commit comments

Comments
 (0)