Day 068-Q1 — 暗黙的Treap(Implicit Treap・区間逆転・列操作)

2026-06-21 赤色 Master / Phase 8+ ★★★★★★★★★ Implicit Treap・split/merge・遅延反転フラグ

問題

長さ $N$ の整数列 $A = [a_1, a_2, \ldots, a_N]$ に対し、$Q$ 個のクエリを処理せよ。

  • クエリ型 1: 1 l r — $A[l..r]$(1-indexed, 閉区間)を逆順にする。
  • クエリ型 2: 2 l r — $A[l..r]$ の最大値を出力する。
  • クエリ型 3: 3 i v — $A[i]$ を $v$ に変更する。

制約

パラメータ範囲
$N$$1 \le N \le 2 \times 10^5$
$Q$$1 \le Q \le 2 \times 10^5$
$a_i, v$$1 \le a_i, v \le 10^9$
$l, r, i$$1 \le l \le r \le N$

入出力例

入力例 1

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

出力例 1

9
7

1 2 5 後: [3,5,1,4,1,9]。最大=9。3 3 7 後: [3,5,7,4,1,9]。1 1 6 後: [9,1,4,7,5,3]。2 2 5 → max(1,4,7,5)=7。

概念図: Implicit Treap の split/merge

split(root, k): 先頭k個と残りに分割 各ノードのフィールド val: 要素値 pri: 乱数優先度(heap性質) sz: 部分木サイズ(添字計算に利用) mx: 部分木最大値 rev: 遅延反転フラグ left, right: 子ノードへのポインタ heap: pri[parent] > pri[child] split(t, k) の判断 lsz = sz(t.left) if lsz >= k: → 左子を split(t.left, k) → t は右側に残る else (lsz < k): → t は左側に入る → 右子を split(t.right, k-lsz-1) 例: [3,1,4,1,5,9] → split(3) → [3,1,4] と [1,5,9] 分割前: 3 1 4 1 5 9 t1 (k=3): 3 1 4 t2: 1 5 9 遅延反転: rev=True → push_down 時に左右子を swap し rev を子に伝播 逆転クエリ [l,r]: split(l-1) → split(r-l+1) → t2.rev^=True → merge

ヒント(段階的開示)

ヒント1: 方向性
区間逆転・最大値クエリ・点更新を同時に扱うには暗黙的Treap(Implicit Treap)が最適。配列の添字を sz から暗黙に計算し、split/merge で $O(\log N)$ 期待値で全クエリを処理できる。
ヒント2: アプローチ
  • 各ノード: val, pri, sz, mx, rev, left, right
  • split(t, k): 左k個 / 残りに分割。左子の sz と k を比較して再帰
  • merge(l, r): pri の大小で根を決め、残りを再帰的にマージ
  • 区間逆転: t2.rev ^= True、push_down 時に左右子を交換
ヒント3: コード骨格
def push_down(t):
    if t and t.rev:
        t.left, t.right = t.right, t.left
        if t.left:  t.left.rev  ^= True
        if t.right: t.right.rev ^= True
        t.rev = False

def split(t, k):
    if t is None: return None, None
    push_down(t)
    lsz = sz(t.left)
    if lsz >= k:
        l, t.left = split(t.left, k)
        pull_up(t); return l, t
    else:
        t.right, r = split(t.right, k - lsz - 1)
        pull_up(t); return t, r

模範解答 (Python)

import sys
import random
input = sys.stdin.readline

class Node:
    __slots__ = ['val','pri','sz','mx','rev','left','right']
    def __init__(self, val):
        self.val = val
        self.pri = random.random()
        self.sz = 1
        self.mx = val
        self.rev = False
        self.left = self.right = None

def _sz(t): return t.sz if t else 0
def _mx(t): return t.mx if t else -1

def push_down(t):
    if t and t.rev:
        t.left, t.right = t.right, t.left
        if t.left:  t.left.rev  ^= True
        if t.right: t.right.rev ^= True
        t.rev = False

def pull_up(t):
    if t:
        t.sz = 1 + _sz(t.left) + _sz(t.right)
        t.mx = max(t.val, _mx(t.left), _mx(t.right))

def split(t, k):
    if t is None:
        return None, None
    push_down(t)
    lsz = _sz(t.left)
    if lsz >= k:
        l, t.left = split(t.left, k)
        pull_up(t)
        return l, t
    else:
        t.right, r = split(t.right, k - lsz - 1)
        pull_up(t)
        return t, r

def merge(l, r):
    if l is None: return r
    if r is None: return l
    push_down(l); push_down(r)
    if l.pri > r.pri:
        l.right = merge(l.right, r)
        pull_up(l)
        return l
    else:
        r.left = merge(l, r.left)
        pull_up(r)
        return r

def main():
    sys.setrecursionlimit(500000)
    N, Q = map(int, input().split())
    A = list(map(int, input().split()))
    root = None
    for v in A:
        root = merge(root, Node(v))

    out = []
    for _ in range(Q):
        line = list(map(int, input().split()))
        if line[0] == 1:
            _, l, r = line
            l -= 1
            t1, t2 = split(root, l)
            t2, t3 = split(t2, r - l)
            t2.rev ^= True
            root = merge(t1, merge(t2, t3))
        elif line[0] == 2:
            _, l, r = line
            l -= 1
            t1, t2 = split(root, l)
            t2, t3 = split(t2, r - l)
            out.append(_mx(t2))
            root = merge(t1, merge(t2, t3))
        else:
            _, i, v = line
            i -= 1
            t1, t2 = split(root, i)
            t2, t3 = split(t2, 1)
            t2.val = v
            pull_up(t2)
            root = merge(t1, merge(t2, t3))

    print('\n'.join(map(str, out)))

main()

Step-by-Step 解説

Step 1: 暗黙的 Treap の基本構造

Treap はランダム優先度 pri で heap 性質を持つ BST。「暗黙的」とは添字をキーとせず、sz(部分木サイズ)から暗黙に位置を計算する点。これにより「k番目の要素」へのアクセスが $O(\log N)$ で可能になる。

Step 2: split(t, k) の動作

左部分木サイズ lsz = sz(t.left) と k を比較する。

条件動作
lsz ≥ kt の添字は k より右 → 左子を再帰 split
lsz < kt 自身が左側の (lsz+1) 番目 → 右子を残り k-lsz-1 個分 split

Step 3: 遅延反転フラグ

t.rev = True はこのノードの部分木が逆順になっていることを示す遅延フラグ。push_down 時に左右子を交換し、子に rev を伝播させることで $O(\log N)$ で区間逆転が実現する。

Step 4: クエリ処理フロー

クエリ手順
逆転 [l,r]split(l-1) → t1, t2 | split(r-l+1) → t2, t3 | t2.rev^=True | merge
最大値 [l,r]split × 2 → t2.mx を読む → merge
点更新 isplit(i-1) → split(1) → t2.val=v, pull_up → merge

Step 5: 計算量

操作計算量
split / merge$O(\log N)$ 期待値
全クエリ$O(Q \log N)$ 期待値
空間$O(N)$

よくあるミス

ミス原因正しい書き方
push_down を忘れるrev フラグが子に伝わらないsplit/merge の先頭で必ず push_down
pull_up を忘れるsz や mx が更新されない子の変更後に必ず pull_up
split の境界値ミス0-indexed/1-indexed の混在split(root, l) で「左 l 個」を意識
再帰深度超過Python のデフォルト制限sys.setrecursionlimit(500000)

次のステップ

発展問題: 暗黙的 Treap で区間ソート(マージソートとの融合)、または永続 Treap(バージョン管理)。関連: RBST(Randomized BST)との比較実装。

自己評価