Day 049-Q1 — Mo's with Rollback(削除不可データ構造の区間クエリ)

2026-06-02 赤色 Master / Phase 8+ ★★★★★★★★★ Mo's Algorithm with Rollback / XOR線形基底

問題

長さ $N$ の数列 $A$ と $Q$ 個の区間クエリ $(l_i, r_i)$ が与えられる。各クエリに対し、部分列 $A[l_i..r_i]$ の XOR 最大部分配列(任意の連続部分列の XOR の最大値)を求めよ。

XOR 最大部分配列は、$A[l..r]$ の prefix XOR 列 $P_0=0, P_1=A[l], P_2=A[l]\oplus A[l+1], \ldots$ において、$P_j \oplus P_i$ ($i \le j$) の最大値として定義される(線形基底を使うと $O(N \log \max A)$ で求まる)。

制約

$1 \le N, Q \le 10^5$
$0 \le A_i < 2^{30}$
$1 \le l_i \le r_i \le N$
時間制限: 3秒

入出力例

入力例 1

5 3
3 5 2 7 1
1 3
2 5
1 5

出力例 1

6
7
7

クエリ $(1,3)$: $A[1..3]=[3,5,2]$, prefix XOR $= [0,3,6,4]$, 最大XOR差 $= 6 \oplus 0 = 6$

概念図: Mo's with Rollback のブロック戦略

数列インデックス(N=10, B=3) Block 0 Block 1 Block 2 1 2 3 4 5 6 7 8 9 10 クエリのソート戦略(Block 0: 右端を単調増加) Q=(1,4) Block0 Q=(2,7) Block0 処理方法:右端は拡張のみ、左端は毎回再構築 右基底: Block境界→r(スナップショットからロールバックして拡張) 左基底: l→Block末尾(毎回新規構築) ⇒ 左基底 + 右基底をマージして XOR 最大値を計算

ヒント(段階的開示)

ヒント1: 方向性
通常の Mo's Algorithm は「追加」と「削除」が必要ですが、XOR 線形基底は削除をサポートしません。このような削除不可データ構造に対して Mo's Algorithm の変形(Mo's with Rollback)が存在します。
ヒント2: アプローチ
  • ブロックサイズ $B=\sqrt{N}$ でクエリを分割
  • 同一ブロック内: 右端を拡張、左端は毎回ブロック境界から再構築
  • ブロック切り替え: 右端基底をブロック境界にリセット(スナップショット)
  • 左端の基底は独立に作成し、右端基底とマージして答えを計算
  • 削除は不要 - 左端基底を毎回捨てて再構築することで代替
ヒント3: XOR 線形基底の実装骨格
class Basis:
    def __init__(self):
        self.b = [0] * 31

    def add(self, x):
        for i in range(30, -1, -1):
            if not (x >> i & 1): continue
            if not self.b[i]:
                self.b[i] = x
                return
            x ^= self.b[i]

    def query_max(self):
        res = 0
        for i in range(30, -1, -1):
            res = max(res, res ^ self.b[i])
        return res

    def copy(self):
        nb = Basis()
        nb.b = self.b[:]
        return nb

模範解答 (Python)

import sys
from bisect import bisect_left

def solve():
    data = sys.stdin.buffer.read().split()
    idx = 0
    N, Q = int(data[idx]), int(data[idx+1]); idx += 2
    A = [int(data[idx+i]) for i in range(N)]; idx += N

    # prefix XOR (P[i] = A[0] XOR ... XOR A[i-1])
    P = [0] * (N + 1)
    for i in range(N):
        P[i+1] = P[i] ^ A[i]

    queries = []
    for i in range(Q):
        l, r = int(data[idx]) - 1, int(data[idx+1]) - 1; idx += 2
        queries.append((l, r))

    class Basis:
        def __init__(self):
            self.b = [0] * 31

        def add(self, x):
            for i in range(30, -1, -1):
                if not (x >> i & 1): continue
                if not self.b[i]:
                    self.b[i] = x
                    return
                x ^= self.b[i]

        def query_max(self):
            res = 0
            for i in range(30, -1, -1):
                res = max(res, res ^ self.b[i])
            return res

        def copy(self):
            nb = Basis()
            nb.b = self.b[:]
            return nb

    ans = [0] * Q
    B = max(1, int(N**0.5))
    order = sorted(range(Q), key=lambda i: (queries[i][0] // B, queries[i][1]))

    for qi in order:
        l, r = queries[qi]
        # Build basis from A[l..r] using prefix XOR
        # XOR max subarray = max over i<=j of P[i] XOR P[j]
        # where P[l..r+1] are the relevant prefix XORs
        final = Basis()
        for i in range(l, r + 2):
            final.add(P[i])
        ans[qi] = final.query_max()

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

solve()

Step-by-Step 解説

1XOR 最大部分配列の本質
区間 $[l, r]$ の XOR 最大部分配列は、prefix XOR 配列 $P[l..r+1]$ を線形基底に入れて query_max を呼ぶだけで求まります($O((r-l+2) \cdot 30)$)。
2Mo's with Rollback の動機
Mo's Algorithm では区間を左右に拡張・縮小しますが、縮小(削除)が線形基底では不可能です。Mo's with Rollback では「右端のみ拡張、左端は毎回再構築」する戦略を取ります。
3クエリのソート
左端のブロック番号でソートし、同一ブロック内では右端を昇順にします。これで右端は単調増加(各ブロック内)、左端は最大 $B$ 回の移動で処理できます。
4右基底と左基底の分離
右基底: ブロック末尾からクエリの右端まで prefix XOR を追加。左基底: クエリの左端からブロック末尾まで独立に構築。最後に両基底をマージして query_max。
5計算量
右端拡張: 各ブロックで $O(N \cdot 30)$, 左端再構築: $O(Q \cdot B \cdot 30)$。全体 $O((N + Q)\sqrt{N} \cdot 30)$。

計算量

各クエリの処理: $O(N \log \max A)$(本実装)
Mo's with Rollback(最適版): $O((N + Q)\sqrt{N} \cdot \log \max A)$
空間: $O(N + Q)$

よくあるミス

ミス原因正しい書き方
prefix XOR の端点を誤るP[i] は A[0..i-1] のXOR区間 [l,r] は P[l..r+1] の基底
削除を試みる線形基底は削除不可Mo with Rollback で再構築
ブロック境界の処理漏れl がブロック末尾と同じ場合l == r のケースを別処理
基底の copy を忘れる破壊的変更でスナップショットが壊れるb[:] でコピーを取る

次のステップ

  • 発展問題: 同様の枠組みで「区間中の線形独立なベクトル数」を求める問題
  • 関連: Day039 Q1(永続線形基底)との比較 — オンライン vs オフライン
  • 応用: Mo with Rollback の枠組みを永続データ構造なしで適用できる問題の発見

自己評価