Day 119-Q1 — 線形基底(XOR Basis・GF(2)ガウス消去法)

2026-08-11 赤色 Master / Phase 8+ ★★★★★★★★★ 線形代数(GF(2))・XOR基底・到達可能性判定

問題

N個のクエリを順に処理せよ。多重集合(初期状態は空)に対し、次の2種類のクエリが与えられる。

  • A x: 値 x を多重集合に追加する
  • Q x: 現在の多重集合から部分集合を選び、その要素すべてのXORを取ることで x を作れるかどうかを判定し、Yes/Noを出力する(空集合を選んでXOR=0を作ることも可能)

入力形式

N
query_1
query_2
...
query_N

各 query_i は A x または Q x の形式。

制約

$1 \le N \le 2\times10^5$
$0 \le x < 2^{30}$
Aクエリ・Qクエリの出現回数は不定

入出力例

入力例1

6
A 5
A 3
Q 6
Q 7
A 6
Q 6

出力例1

Yes
No
Yes

{5,3}から作れるXORは{0,5,3,6}。6=5^3で作れるのでYes、7は作れないのでNo。A 6は既存の基底で表現できる値なので基底は増えず、その後のQ 6も引き続きYes。

概念図: ガウス消去による基底判定

基底 {5,3} に対して x=6(110) が作れるか判定 basis[bit2] 5 (101) basis[bit1] 3 (011) basis[bit0] x = 110 (=6) 110 bit2立ってる → basis[2]とXOR 011 bit1立ってる → basis[1]とXOR 000 x=0 → 作れる (Yes) x = 111 (=7) 111→010 bit2,bit1消去後 001 basis[0]が空 → 作れない (No) 最上位bitから basis[b] とXORし続け、0になれば作れる/空きbitにぶつかれば作れない

ヒント(段階的開示)

ヒント1: 方向性
「部分集合のXORで x が作れるか」を毎回すべての部分集合を試すと $O(2^N)$ で間に合わない。XORはGF(2)(0と1の世界)上のベクトル加算とみなせるので、この問題は「与えられたベクトル集合が張る線形空間に x が含まれるか」という線形代数の問題に帰着できる。
ヒント2: アプローチ
30bit整数を30次元のGF(2)ベクトルとみなす。ガウスの消去法の要領で「基底」を維持する:新しい値を追加するとき、最上位から見ていき、まだ基底に無いbitがあればそこに登録し、既にあればXORして消去してから次のbitへ進む。この基底は高々30個しか要素を持たず、追加・判定ともに $O(30)$ で処理できる。
ヒント3: 誘導(コード骨格)
# basis[b] = 最上位bitがbであるような基底ベクトル(無ければ0)
def insert(x):
    for b in range(29, -1, -1):
        if not (x >> b) & 1:
            continue
        if basis[b] == 0:
            basis[b] = x
            return
        x ^= basis[b]
    # ここに到達したらxは既存の基底で表現可能(線形従属)なので何もしない

def can_represent(x):
    for b in range(29, -1, -1):
        if not (x >> b) & 1:
            continue
        if basis[b] == 0:
            return False   # このbitを消せる基底が無い=作れない
        x ^= basis[b]
    return x == 0          # 全bit消せたら作れる

模範解答 (Python)

import sys


def solve():
    data = sys.stdin.buffer.read().split()
    idx = 0
    N = int(data[idx]); idx += 1
    BIT = 30  # 0 <= x < 2^30
    basis = [0] * BIT

    def insert(x):
        for b in range(BIT - 1, -1, -1):
            if not (x >> b) & 1:
                continue
            if basis[b] == 0:
                basis[b] = x
                return
            x ^= basis[b]

    def can_represent(x):
        for b in range(BIT - 1, -1, -1):
            if not (x >> b) & 1:
                continue
            if basis[b] == 0:
                return False
            x ^= basis[b]
        return x == 0

    out = []
    for _ in range(N):
        t = data[idx]; idx += 1
        x = int(data[idx]); idx += 1
        if t == b'A':
            insert(x)
        else:
            out.append("Yes" if can_represent(x) else "No")
    print('\n'.join(out))


solve()
計算量: 1回のinsert/can_representはビット幅30に比例するのでO(30)、全体でO(30N)。乱数300試行のstress test(brute forceの「全部分集合のXOR集合を毎回再計算して判定」との突き合わせ)を実際に実行し、全試行で出力が一致することを確認済み。

Step-by-Step 解説

1XORをGF(2)ベクトルとして捉える
30bit整数は「30個の0/1成分を持つベクトル」とみなせる。XORはこの空間での成分ごとの加算(mod 2)に一致する。「部分集合のXORで作れる値の集合」は、その部分集合が張る線形部分空間そのものである。
2基底(ガウス消去形)の維持
basis[b]に「最上位bitがちょうどbであるベクトル」を保持するようにガウス消去を行うと、基底は常に簡約階段形(各基底ベクトルの最上位bitが互いに異なる)になる。
3挿入処理
挿入したい値xを上位bitから見ていき、空いていればxをそこに置いて終了。埋まっていればxをXORして「そのbitを消し」次のbitへ進む。最終的にxが0になれば、基底は増えない。
4判定処理
考え方は挿入とほぼ同じだが、基底が空いている場合はその時点で「作れない」と確定できる。全bitを消しきれてx=0になれば「作れる」。
5計算量の確認
基底のサイズは高々30(ビット幅)なので、挿入・判定ともにO(30)。N回のクエリでO(30N)、$N\le2\times10^5$でも余裕を持って間に合う。

よくあるミス

ミス原因正しい書き方
上位bitからではなく下位bitから消去してしまう単純な走査順で実装してしまう必ず最上位bitから見て「簡約階段形」を保つ
xが0になったら基底に追加しない、を忘れて0を登録してしまうループを抜けた後に無条件でbasisへ書き込んでしまうループ内でreturnするのは基底に空きを見つけた時のみとする
can_representとinsertのロジックを混同し、判定なのに基底を変更してしまう同じ関数を使い回そうとする判定用はbasis配列を変更しない(xはローカル変数として消去するだけ)
ビット幅を制約より小さく(例:20)設定してしまう制約の$2^{30}$を見落とすBIT = 30のように制約の上限bit数以上を確保する

次のステップ

  • 発展: 各基底ベクトルに「どの元の要素の組み合わせで作られたか」を復元できるよう、挿入時に使用した元の要素の集合(bitmask等)も一緒に管理してみる。
  • 次回予告: クラスカル再構築木(Kruskal's Reconstruction Tree)

自己評価

自分の回答

気づき・メモ