Day 092-Q1 — 永続セグメント木(区間 $k$ 番目・オンライン)

2026-07-15 赤色 Master / Phase 8+ ★★★★★★★★★ Persistent Segtree・区間k番目

問題

長さ $N$ の数列 $a_1, a_2, \dots, a_N$ が与えられる。$Q$ 個のクエリ l r k について、区間 $[l, r]$ に含まれる値のうち $k$ 番目に小さい値 を出力せよ。

制約

パラメータ範囲備考
$N, Q$$1 \le N, Q \le 2\times10^5$要素数・クエリ数
$a_i$$1 \le a_i \le 10^9$要素の値(座標圧縮対象)
$l, r, k$$1 \le l \le r \le N,\ 1 \le k \le r-l+1$区間と順位

入出力例

入力例1

5 3
1 5 2 3 4
1 5 3
2 4 1
3 5 2

出力例1

3
2
3

$[1,5]$ ソートで3番目=3。$[2,4]=(5,2,3)$ の1番目=2。$[3,5]=(2,3,4)$ の2番目=3。

概念図

各接頭辞に頻度木の「版」を持ち、root[r] − root[l−1] で区間頻度 版 (root) root[l-1] root[r] ← この2版を並行降下 木上二分探索 [0,4] [0,2] [3,4] [0,1] [2,2] leftcnt = cnt[左(r)] − cnt[左(l−1)] k ≤ leftcnt → 左へ それ以外 → k−=leftcnt, 右へ 葉 [2,2] = ランク2 → 値 3

ヒント

ヒント1(方向性)

「区間の $k$ 番目」は値でソートした累積個数の二分探索。接頭辞 $[1,i]$ の「値の出現頻度」を持てば、区間 $[l,r]$ の頻度は差分で得られる。

ヒント2(アプローチ)

値域を座標圧縮し、接頭辞ごとに頻度セグメント木を作る。$N$ 本独立ではメモリ $O(N^2)$。永続化で前版とノード共有すれば更新1回 $O(\log N)$ ノード。

ヒント3(ほぼ答え)
while lo < hi:
    leftcnt = cnt[lc[v]] - cnt[lc[u]]
    if k <= leftcnt:
        u, v, hi = lc[u], lc[v], mid
    else:
        k -= leftcnt
        u, v, lo = rc[u], rc[v], mid + 1

模範解答

import sys

def main():
    data = sys.stdin.buffer.read().split()
    idx = 0
    n = int(data[idx]); idx += 1
    q = int(data[idx]); idx += 1
    a = [int(data[idx + i]) for i in range(n)]; idx += n

    xs = sorted(set(a))
    comp = {v: i for i, v in enumerate(xs)}
    m = len(xs)

    MAXN = n * 20 + 10
    lc = [0] * MAXN
    rc = [0] * MAXN
    cnt = [0] * MAXN
    cur = 0  # node 0 = 空ノード(共有)

    roots = [0] * (n + 1)

    def update(prev, lo, hi, pos):
        nonlocal cur
        cur += 1
        node = cur
        lc[node] = lc[prev]; rc[node] = rc[prev]; cnt[node] = cnt[prev] + 1
        if lo == hi:
            return node
        mid = (lo + hi) // 2
        if pos <= mid:
            lc[node] = update(lc[prev], lo, mid, pos)
        else:
            rc[node] = update(rc[prev], mid + 1, hi, pos)
        return node

    for i in range(1, n + 1):
        roots[i] = update(roots[i - 1], 0, m - 1, comp[a[i - 1]])

    out = []
    for _ in range(q):
        l = int(data[idx]); r = int(data[idx + 1]); k = int(data[idx + 2]); idx += 3
        u = roots[l - 1]; v = roots[r]
        lo, hi = 0, m - 1
        while lo < hi:
            mid = (lo + hi) // 2
            leftcnt = cnt[lc[v]] - cnt[lc[u]]
            if k <= leftcnt:
                u, v, hi = lc[u], lc[v], mid
            else:
                k -= leftcnt; u, v, lo = rc[u], rc[v], mid + 1
        out.append(str(xs[lo]))
    sys.stdout.write('\n'.join(out) + '\n')

main()

計算量: 構築 $O(N\log N)$、各クエリ $O(\log N)$、合計 $O((N+Q)\log N)$。

Step-by-Step 解説

Step 1: 座標圧縮

値は最大 $10^9$ だが種類は高々 $N$。sorted(set(a)) で圧縮し、葉をランクに対応させる。

Step 2: 永続更新でノード共有

変更しない側の子は前版のノードをそのまま指す。1回の挿入で作る新ノードは根から葉までの $O(\log N)$ 個だけ。

Step 3: 版の差分で区間頻度

意味
cnt[lc[v]] - cnt[lc[u]]区間 $[l,r]$ の左部分木側の要素数
v = root[r], u = root[l-1]接頭辞頻度木の2版

Step 4: 木上の二分探索でk番目

左の個数 leftcnt が $k$ 以上なら左、そうでなければ $k$ から引いて右へ。葉に着いたランクを値に戻す。

よくあるミス

ミス原因正しい書き方
メモリ不足 (MLE)ノード配列が小さいMAXN = N*(⌈log₂M⌉+2) 程度
空ノードの破壊node 0 を更新node 0 は cnt=lc=rc=0 の共有ダミー固定
区間を root[l]〜root[r] にする差分がずれる正しくは root[l-1]root[r]
k を 0-indexed で処理1個ずれる$k$ は1始まり、k <= leftcnt

次のステップ

  • 発展: 区間内で $x$ 以下の値の個数、区間中央値、区間相異なる値
  • 発展: オフラインなら Merge Sort Tree / Wavelet Matrix でも解ける(比較)
  • 次回予告: Hopcroft-Karp(最大二部マッチング)

自己評価

理解度: / /

自分の回答:

気づき・メモ: