Day 084-Q3 — 乱択クイックセレクト + Median-of-Medians BFPRT(線形時間選択・第 $k$ 小要素)

2026-07-07 赤色 Master / Phase 8+ ★★★★★★★★★ Quickselect・BFPRT・選択問題

問題

長さ $N$ の整数列 $A$ が与えられる。$Q$ 個のクエリ $(k_i)$ が与えられ、各クエリで $A$ の $k_i$ 番目に小さい要素(1-indexed)を答えよ。

パート1: 乱択クイックセレクト(Randomized Quickselect)を実装し、期待 $O(N)$ でクエリを処理せよ。

パート2: 決定論的線形時間選択アルゴリズム(Median-of-Medians / BFPRT)を実装し、最悪 $O(N)$ でクエリを処理せよ。

制約

パラメータ範囲備考
$N$$\le 5 \times 10^5$配列長
$Q$$\le 10^5$クエリ数
$k_i$$1 \le k_i \le N$1-indexed
$A_i$$-10^9 \le A_i \le 10^9$整数

入出力例

入力例1

8 5
3 1 4 1 5 9 2 6
1
4
7
8
3

出力例1

1
3
6
9
2

概念図: クイックセレクトと BFPRT の比較

Quickselect vs BFPRT — 第4小要素を探す 乱択クイックセレクト 入力: [3,1,4,1,5,9,2,6], k=4 3 1 4 pivot 1 5 9 2 6 pivot=4 で分割: left=[3,1,1,2] (4個) mid=[4] (1個) right=[5,9,6] (3個) |left|=4 >= k=4 → left へ再帰 再帰: [3,1,1,2], k=4 pivot=2: left=[1,1] mid=[2] right=[3] |left|+|mid|=3 < k=4 → right へ k=1 答え = 3 ✓ BFPRT (Median-of-Medians) 5要素グループに分割 3 1 4 1 5 中央値=3 9 2 6 中央値=6 medians = [3, 6] 中央値の中央値 = 中央値([3,6]) = 3 or 6 ピボット = 3 (保証: 常に30-70%の範囲) left=[1,1,2] mid=[3] right=[4,5,9,6] |left|=3 < k=4 ≤ |left|+|mid|=4 答え = mid[0] = 3 ✓ 計算量比較 乱択: 期待 O(N)、定数小、実用的 BFPRT: 最悪 O(N)、定数大、理論的

ヒント

ヒント1(方向性)

「第 $k$ 小の要素を求める」問題は選択問題(Selection Problem)。ソートすれば $O(N \log N)$ だが、$O(N)$ で解ける。乱択クイックセレクトは期待 $O(N)$、BFPRT は最悪 $O(N)$(定数が大きい)。

ヒント2(アプローチ)

乱択クイックセレクト:(1) ランダムにピボットを選ぶ (2) left(小)/ mid(等)/ right(大)に分割 (3) $k \le |left|$ なら left に再帰、$k \le |left|+|mid|$ ならピボットが答え、それ以外は right に再帰

ヒント3(ほぼ答え)
import random

def quickselect(arr, k):  # k: 0-indexed
    while True:
        if len(arr) == 1:
            return arr[0]
        pivot = arr[random.randint(0, len(arr) - 1)]
        left  = [x for x in arr if x < pivot]
        mid   = [x for x in arr if x == pivot]
        right = [x for x in arr if x > pivot]
        if k < len(left):
            arr = left
        elif k < len(left) + len(mid):
            return pivot
        else:
            k -= len(left) + len(mid)
            arr = right

模範解答

import sys
import random
input = sys.stdin.readline

def solve():
    N, Q = map(int, input().split())
    A = list(map(int, input().split()))

    def quickselect(arr, k):
        """期待 O(N) 乱択クイックセレクト (0-indexed k)"""
        while True:
            if len(arr) == 1:
                return arr[0]
            pivot = arr[random.randint(0, len(arr) - 1)]
            left  = [x for x in arr if x < pivot]
            mid   = [x for x in arr if x == pivot]
            right = [x for x in arr if x > pivot]
            if k < len(left):
                arr = left
            elif k < len(left) + len(mid):
                return pivot
            else:
                k -= len(left) + len(mid)
                arr = right

    def median_of_medians(arr, k):
        """最悪 O(N) BFPRT アルゴリズム (0-indexed k)"""
        if len(arr) <= 5:
            return sorted(arr)[k]
        medians = []
        for i in range(0, len(arr), 5):
            chunk = sorted(arr[i:i+5])
            medians.append(chunk[len(chunk) // 2])
        pivot = median_of_medians(medians, len(medians) // 2)
        left  = [x for x in arr if x < pivot]
        mid   = [x for x in arr if x == pivot]
        right = [x for x in arr if x > pivot]
        if k < len(left):
            return median_of_medians(left, k)
        elif k < len(left) + len(mid):
            return pivot
        else:
            return median_of_medians(right, k - len(left) - len(mid))

    out = []
    for _ in range(Q):
        k = int(input()) - 1  # 0-indexed
        out.append(quickselect(A[:], k))

    sys.stdout.write('\n'.join(map(str, out)) + '\n')

solve()

Step-by-Step 解説

Step 1: クイックセレクトの期待計算量

毎回平均で半分に縮小するため $T(N) = T(N/2) + O(N) = O(N)$。最悪の場合は $O(N^2)$ だが、ランダムピボット選択により確率的に回避。

Step 2: BFPRT の最悪 $O(N)$ 保証

手順内容計算量
グループ化5要素ずつ $N/5$ グループ$O(N)$
中央値算出各グループを定数時間でソート$O(N)$
中央値の中央値$N/5$ 要素に再帰$T(N/5)$
分割ピボットは 30-70% 保証$O(N)$
再帰最悪 $7N/10$ 要素$T(7N/10)$

再帰式: $T(N) = T(N/5) + T(7N/10) + O(N) = O(N)$

Step 3: 実用上の選択

アルゴリズム計算量実用速度用途
ソート$O(N \log N)$速い複数クエリ
乱択 Quickselect期待 $O(N)$非常に速い競プロ向き
BFPRT最悪 $O(N)$遅い(定数大)理論的最適

よくあるミス

ミス原因正しい書き方
1-indexed / 0-indexed の混在クエリ $k$ を変換し忘れるk -= 1 で 0-indexed に統一
pivot 選択で最悪ケース常に最小や最大を選ぶ実装random.randint でランダム選択
元の配列を破壊するarr を直接変更A[:] でコピーしてから渡す

次のステップ

  • 発展問題: ストリーミング中央値(Dual Heap: Max-heap + Min-heap)
  • 発展問題: ウェイトつき選択問題(重み付き中央値)

自己評価

理解度: / /

自分の回答:

気づき・メモ: