Day 076-Q2 — Mo's Algorithm with Updates(更新付き Mo 法・区間異なる値の種類数 $O(Q^{5/3})$)

2026-06-29 赤色 Master / Phase 8+ ★★★★★★★★★ Mo's with Updates・3次元Mo法

問題

長さ $N$ の数列 $A$ が与えられる。以下の $Q$ 個のクエリを処理せよ:

  • クエリ P: P l r — 区間 $[l, r]$(1-indexed)に含まれる異なる値の種類数を出力する
  • クエリ U: U i x — $A_i$ を $x$ に変更する

制約

パラメータ範囲備考
$N$$1 \le N \le 10^5$数列の長さ
$Q$$1 \le Q \le 10^5$クエリ数
$A_i, x$$1 \le A_i, x \le 10^5$要素値

入出力例

入力例1

6 4
1 2 3 1 2 3
P 1 6
U 2 4
P 1 6
P 2 5

出力例1

3
4
3

初期: [1,2,3,1,2,3] → P(1,6): {1,2,3}=3種。U(2,4): [1,4,3,1,2,3]。P(1,6): {1,2,3,4}=4種。P(2,5): {4,3,1,2}=4種? → {1,2,3,4}のうち存在→3種(4,3,1,2)=4種。

概念図: 3 次元 Mo 法のソート

Mo's Algorithm with Updates — ソート軸 第1軸: ⌊l/B⌋ 左端のブロック番号 B = Q^(2/3) 第2軸: ⌊r/B⌋ 右端のブロック番号 同ブロック内でソート 第3軸: t 直前の更新回数(時刻) 時刻順に処理 計算量の解析 l の移動: O(Q × B) — 左端が同ブロック内で動く r の移動: O(Q × B) — 右端が同ブロック内で動く t の移動: O(Q × (N/B²) × B) = O(QN/B) B = Q^(2/3) で最適化: O(Q × Q^(2/3)) = O(Q^(5/3))

3次元Mo法では $(l//B,\; r//B,\; t)$ の順でクエリをソートする。ブロックサイズ $B = Q^{2/3}$ のとき全移動コストが $O(Q^{5/3})$ になる。

ヒント

ヒント1(方向性)

通常の Mo 法 $O((N+Q)\sqrt{N})$ は点更新があると壊れる。更新クエリを「時刻軸」として3次元に拡張し、ブロックサイズを $B = Q^{2/3}$ にすることで $O(Q^{5/3})$ に抑える。これが Mo's Algorithm with Updates。

ヒント2(アプローチ)
  1. クエリを読みながら更新と区間クエリを分離し、各区間クエリに「直前の更新回数 $t$」を付ける
  2. 区間クエリを $(l//B, r//B, t)$ でソート
  3. 現在の $(cl, cr, ct)$ から $(l, r, t)$ へ移動: 時刻 $t$ を先に調整し、次に $l, r$ を伸縮
  4. 時刻移動: $A\_state[idx]$ が区間内にあるとき add/remove; $A\_state$ を書き換え
ヒント3(ほぼ答え)
B = max(1, int(len(point_queries) ** (2/3)))
indexed.sort(key=lambda x: (x[0]//B, x[1]//B, x[2]))

# 時刻を合わせる (前へ)
while ct < t:
    idx, old_v, new_v = updates[ct]
    if cl <= idx <= cr:
        remove(A_state[idx]); add(new_v)
    A_state[idx] = new_v
    ct += 1
# 時刻を合わせる (後へ)
while ct > t:
    ct -= 1
    idx, old_v, new_v = updates[ct]
    if cl <= idx <= cr:
        remove(A_state[idx]); add(old_v)
    A_state[idx] = old_v

模範解答

import sys
input = sys.stdin.readline

def main():
    N, Q = map(int, input().split())
    A = list(map(int, input().split()))
    VMAX = 100001

    updates = []
    point_queries = []
    A_cur = A[:]

    for _ in range(Q):
        line = input().split()
        if line[0] == 'U':
            i, x = int(line[1]) - 1, int(line[2])
            updates.append((i, A_cur[i], x))
            A_cur[i] = x
        else:
            l, r = int(line[1]) - 1, int(line[2]) - 1
            point_queries.append((l, r, len(updates)))

    if not point_queries:
        return

    B = max(1, int(len(point_queries) ** (2/3)))
    indexed = [(l, r, t, qi) for qi, (l, r, t) in enumerate(point_queries)]
    indexed.sort(key=lambda x: (x[0]//B, x[1]//B, x[2]))

    A_state = A[:]
    cnt = [0] * VMAX
    distinct = 0

    def add(v):
        nonlocal distinct
        if cnt[v] == 0: distinct += 1
        cnt[v] += 1

    def remove(v):
        nonlocal distinct
        cnt[v] -= 1
        if cnt[v] == 0: distinct -= 1

    cl, cr, ct = 0, -1, 0
    answers = [0] * len(point_queries)

    for l, r, t, qi in indexed:
        while ct < t:
            idx, old_v, new_v = updates[ct]
            if cl <= idx <= cr:
                remove(A_state[idx]); add(new_v)
            A_state[idx] = new_v
            ct += 1
        while ct > t:
            ct -= 1
            idx, old_v, new_v = updates[ct]
            if cl <= idx <= cr:
                remove(A_state[idx]); add(old_v)
            A_state[idx] = old_v
        while cr < r:
            cr += 1; add(A_state[cr])
        while cl > l:
            cl -= 1; add(A_state[cl])
        while cr > r:
            remove(A_state[cr]); cr -= 1
        while cl < l:
            remove(A_state[cl]); cl += 1
        answers[qi] = distinct

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

main()

Step-by-Step 解説

Step 1: クエリの分類と時刻付け

更新クエリを updates リストに順番に記録する。各区間クエリには「直前の更新回数 $t$」を付与する。これが3次元目の軸になる。

Step 2: ブロックサイズ $B = Q^{2/3}$

通常の Mo 法では $B = \sqrt{N}$ が最適だが、時刻軸が加わると最適 $B$ が変わる。区間移動コスト $O(QB)$ と時刻移動コスト $O(QN/B)$ をバランスさせると $B = N^{1/2}$ ではなく $Q^{2/3}$ が最適。

Step 3: ソート順

$(l//B, r//B, t)$ の辞書順でソートする。これにより各軸の移動の合計が $O(Q^{5/3})$ に抑えられる。

Step 4: 時刻の前後移動

時刻を 1 進める: 更新インデックスが現在の区間 $[cl, cr]$ 内なら値を変更して add/remove。$A\_state$ も書き換える。時刻を 1 戻す: ct -= 1 してから old\_v に戻す(逆順)。

Step 5: 区間の伸縮と集計

通常の Mo 法と同様に $cl, cr$ を1ずつ伸縮し distinct(異なる値の種類数)を管理する。

よくあるミス

ミス原因正しい書き方
時刻移動の順序$l, r$ を先に動かすと不整合時刻を先に合わせてから $l, r$ を調整
ロールバック時の old/new 逆転ct -= 1 する前に値を参照ct -= 1 してから updates[ct] を参照
ブロックサイズ$\sqrt{N}$ を使うと $O(Q^2)$ になる$Q^{2/3}$ を使う

次のステップ

  • 発展問題: 「更新付き区間 k 番目要素クエリ」→ Mo's with Updates + 二分探索($O(Q^{5/3} \log N)$)

自己評価

理解度:

自分の回答:

気づき・メモ: