問題
整数列のバージョン管理と区間 $k$ 番目クエリを処理せよ。
S v i x: バージョン $v$ の $A_i$ を $x$ に変更した新バージョンを生成し番号を出力Q v l r k: バージョン $v$ の $A[l..r]$ の $k$ 番目に小さい値を出力
制約
| パラメータ | 範囲 |
|---|---|
| $N$ | $1 \le N \le 10^5$ |
| $Q$ | $1 \le Q \le 10^5$ |
| $A_i, x$ | $1 \le A_i, x \le 10^9$ |
| $k$ | $1 \le k \le r - l + 1$ |
入出力例
入力例 1
5 5
3 1 4 1 5
Q 0 1 5 3
S 0 3 2
Q 1 1 5 3
Q 0 2 4 2
S 1 1 10
出力例 1
3
1
4
2
バージョン0: [3,1,4,1,5]。全体の3番目に小さい値=3。
バージョン1 = v0の3番目を2に: [3,1,2,1,5]。3番目に小さい=4。
概念図: 永続セグメント木の構造
ヒント(段階的開示)
ヒント1: 方向性
永続セグメント木(値域でカウント)を使う。バージョン $v$ の prefix $[1, j]$ の永続セグ木根を保持し、差分で区間 $[l, r]$ の値分布を表現。$k$ 番目は左右分岐で $O(\log M)$。
ヒント2: アプローチ
- 座標圧縮: 全値(更新値含む)を $[0, M)$ に圧縮
- 永続セグ木:
update(prev, pos)でパスを $O(\log M)$ ノードコピーして新根を返す roots[j]= $A_0, \ldots, A_{j-1}$ 挿入後の根- 区間 $[l, r]$ の $k$ 番目 =
kth(roots[l], roots[r+1], 0, M, k)
ヒント3: コード骨格
def update(prev, lo, hi, pos):
global nc
cur = nc; nc += 1
Lc[cur]=Lc[prev]; Rc[cur]=Rc[prev]; cnt[cur]=cnt[prev]+1
if hi - lo == 1: return cur
mid = (lo + hi) >> 1
if pos < mid:
Lc[cur] = update(Lc[prev], lo, mid, pos)
else:
Rc[cur] = update(Rc[prev], mid, hi, pos)
return cur
def kth(u, v, lo, hi, k):
if hi - lo == 1: return lo
mid = (lo + hi) >> 1
lc = cnt[Lc[v]] - cnt[Lc[u]]
if k <= lc:
return kth(Lc[u], Lc[v], lo, mid, k)
return kth(Rc[u], Rc[v], mid, hi, k - lc)
模範解答 (Python)
import sys
from sys import setrecursionlimit
input = sys.stdin.readline
setrecursionlimit(500000)
def solve():
N, Q = map(int, input().split())
A = list(map(int, input().split()))
ops = []
extra_vals = []
for _ in range(Q):
line = input().split()
if line[0] == 'S':
v, i, x = int(line[1]), int(line[2])-1, int(line[3])
ops.append(('S', v, i, x))
extra_vals.append(x)
else:
v, l, r, k = int(line[1]), int(line[2])-1, int(line[3])-1, int(line[4])
ops.append(('Q', v, l, r, k))
all_vals = sorted(set(A + extra_vals))
comp = {v: i for i, v in enumerate(all_vals)}
M = len(all_vals)
MAXNODE = (N + Q) * 20 + 10
Lc = [0] * MAXNODE
Rc = [0] * MAXNODE
cnt = [0] * MAXNODE
nc = 1
def update(prev, lo, hi, pos):
nonlocal nc
cur = nc; nc += 1
Lc[cur] = Lc[prev]; Rc[cur] = Rc[prev]; cnt[cur] = cnt[prev] + 1
if hi - lo == 1:
return cur
mid = (lo + hi) >> 1
if pos < mid:
Lc[cur] = update(Lc[prev], lo, mid, pos)
else:
Rc[cur] = update(Rc[prev], mid, hi, pos)
return cur
def kth(u, v, lo, hi, k):
if hi - lo == 1:
return lo
mid = (lo + hi) >> 1
lc = cnt[Lc[v]] - cnt[Lc[u]]
if k <= lc:
return kth(Lc[u], Lc[v], lo, mid, k)
return kth(Rc[u], Rc[v], mid, hi, k - lc)
ver_arrays = [A[:]]
ver_id = 0
out = []
for op in ops:
if op[0] == 'S':
_, v, i, x = op
new_arr = ver_arrays[v][:]
new_arr[i] = x
ver_arrays.append(new_arr)
ver_id += 1
out.append(str(ver_id))
else:
_, v, l, r, k = op
arr = ver_arrays[v]
roots = [0] * (N + 1)
for j in range(N):
roots[j+1] = update(roots[j], 0, M, comp[arr[j]])
idx = kth(roots[l], roots[r+1], 0, M, k)
out.append(str(all_vals[idx]))
print('\n'.join(out))
solve()
Step-by-Step 解説
Step 1: 座標圧縮
全バージョンで出現する値を収集し、$[0, M)$ に圧縮する。更新クエリの値も含めること。
Step 2: 永続セグメント木の更新
update(prev, lo, hi, pos) は根から葉までのパス上のノードをコピーして新根を返す。$O(\log M)$ ノードを生成。
Step 3: prefix 累積
roots[j+1] = update(roots[j], 0, M, comp[arr[j]]) で $j$ 番目の要素を追加した prefix セグ木を構築。
Step 4: k番目クエリ
2つの根 $u$($[1, l)$ の累積)と $v$($[1, r+1)$ の累積)の差分が区間 $[l, r]$ の値分布。左部分木のカウント差と $k$ を比較して二分決定。
計算量
| 処理 | 計算量 |
|---|---|
| prefix セグ木構築(1回) | $O(N \log M)$ |
| k番目クエリ | $O(\log M)$ |
| 全体(簡略版) | $O(Q \cdot N \log M)$(各 Q クエリで prefix 再構築) |
| 最適版(永続点更新) | $O((N + Q) \log M)$ 全体 |
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| MAXNODE 不足 | 挿入1回で $O(\log M)$ ノード | (N + Q) * 20 程度で確保 |
| kth の引数順 | $u$ と $v$ を逆にする | $u$ = 小さい側(除外)、$v$ = 大きい側 |
| 更新値の圧縮漏れ | S クエリの $x$ を all_vals に含めない | 初期化時に全 $x$ を収集して圧縮 |
次のステップ
- 永続 Treap による動的 insert/delete + k番目クエリ
- HLD + 永続セグ木によるパス上 $k$ 番目クエリ
自己評価
自分の回答:
気づき・メモ: