アルゴリズム概要

cumsum
累積和
searchsorted
二分探索
O(N log N)
時間計算量
O(N)
空間計算量

📥 入力例

person_id person_name weight turn
5 Alice 250 1
3 Alex 350 2
6 John Cena 400 3
2 Marie 200 4
4 Bob 175 5
1 Winston 500 6

🟢 乗車可能  /  🔴 超過で乗車不可

📤 累積重量トレース

turn name weight cum_w 状態
1 Alice 250 250
2 Alex 350 600
3 John Cena 400 1000 ✅🏆
4 Marie 200 1200
🎯 出力: John Cena

🔑 解法の核心:二分探索が使える理由

全ての weight > 0 が保証されているため、cumsum(累積和)は 単調増加 が確定します。 単調増加列に対しては np.searchsorted による二分探索 O(log N) が適用でき、 線形探索 O(N) から高速化できます。

ステップバイステップ解説

Python / pandas 実装

改善案① numpy + searchsorted Beats ~95%
import pandas as pd
import numpy as np

def last_passenger(queue: pd.DataFrame) -> pd.DataFrame:
    """
    turn(1〜N の連続整数)を 0-indexed の配列インデックスとして直接利用。
    ソートを O(N log N) argsort → O(N) 直接配置に置換し、
    線形探索を O(log N) searchsorted に置換する。

    Returns:
        pd.DataFrame: 列 ['person_name'] の 1 行
    """
    n = len(queue)

    # numpy 配列に一括変換(pandas オーバーヘッドを排除)
    turns   = queue['turn'].to_numpy() - 1    # 0-indexed に変換
    weights = queue['weight'].to_numpy()
    names   = queue['person_name'].to_numpy()

    # 🔑 turn を直接インデックスとして使い O(N) で配置(argsort 不要)
    weights_sorted = np.empty(n, dtype=np.int64)
    names_sorted   = np.empty(n, dtype=object)
    weights_sorted[turns] = weights
    names_sorted[turns]   = names

    # 累積和(単調増加が確定)
    cum_w = weights_sorted.cumsum()           # O(N)

    # 🔑 searchsorted: 単調増加列への二分探索 O(log N)
    # side='right': 1000 より大きくなる最初の位置 → -1 で最後の有効位置
    last_pos = np.searchsorted(cum_w, 1000, side='right') - 1

    if last_pos < 0:
        return pd.DataFrame({'person_name': []})

    return pd.DataFrame({'person_name': [names_sorted[last_pos]]})


# ─────────────────────────────────────────────
# 別解: 可読性重視(Beats ~83%)
# ─────────────────────────────────────────────
def last_passenger_readable(queue: pd.DataFrame) -> pd.DataFrame:
    return (
        queue
        .sort_values('turn')                        # 計算用ソート
        .assign(cum_w=lambda df: df['weight'].cumsum())
        .loc[lambda df: df['cum_w'].le(1000), ['person_name']]
        .tail(1)
        .reset_index(drop=True)
    )

SQL (PostgreSQL 16.6+)

WITH cum AS NOT MATERIALIZED (
  SELECT
    person_name,
    turn,
    SUM(weight) OVER (
      ORDER BY turn
      ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cum_w
  FROM Queue
)
SELECT person_name
FROM cum
WHERE cum_w <= 1000
ORDER BY cum_w DESC   -- 単調増加のため turn DESC と等価、再ソートを省略
LIMIT 1;

処理フローチャート

開始 ① 入力 DataFrame 受け取り Queue(person_id, person_name, weight, turn) ② numpy 配列へ一括変換 turns = queue['turn'].to_numpy() - 1 weights = queue['weight'].to_numpy() names = queue['person_name'].to_numpy() ③ turn を index として直接配置 O(N) weights_sorted[turns] = weights names_sorted[turns] = names ※ argsort 不要 / ソート O(N log N) を O(N) に削減 ④ 累積和の計算 O(N) cum_w = weights_sorted.cumsum() 単調増加が確定 → 二分探索適用可能 ⑤ 二分探索で境界を O(log N) 発見 pos = np.searchsorted(cum_w, 1000, side='right') last_pos = pos - 1 例: [250,600,1000,1200,…] → pos=3 → last_pos=2 ⑥ 該当人物名を返却 names_sorted[last_pos] → person_name 終了 計算量サマリ 直接配置 O(N) + cumsum O(N) + searchsorted O(log N) 全体: O(N) ← 旧 O(N log N) から改善 ※ turn が 1〜N の連続整数という制約を最大活用

フローの説明:
1. DataFrame を numpy 配列に変換し pandas オーバーヘッドを排除します。
2. turn が 1〜N の連続整数であることを利用し、直接インデックスとして配置(O(N))することで argsort を不要にします。
3. cumsum() で単調増加な累積和を O(N) で生成します。
4. np.searchsorted(..., side='right') - 1 で二分探索 O(log N) により最後に乗れる位置を特定します。
5. 対応する人物名を返却します。

計算量分析

処理ステップ 旧実装 改善後 改善ポイント
ソート処理 O(N log N)
pandas sort_values
O(N)
直接インデックス配置
turn が 1〜N 連続整数の制約を活用
累積和 O(N) O(N) numpy cumsum で同等、オーバーヘッド削減
最終行の探索 O(N)
le(1000).tail(1) 線形
O(log N)
searchsorted 二分探索
単調増加性を利用した二分探索
合計(時間) O(N log N) O(N) ボトルネックのソートを完全に排除
空間計算量 O(N) O(N) numpy 配列 3本(weights, names, cum_w)
O(N)
最終時間計算量
旧 O(N log N) から改善
O(log N)
searchsorted 探索
旧 O(N) 線形探索から改善
~95%
推定 Beats スコア
旧 ~83% から大幅改善