E - 通信モードの切り替え / Switching Communication Modes 解説 by admin
claude4.8opus-high概要
各送信機をモード A / B のどちらかに設定したとき、稼働させる送信機の集合を自由に選んで得られる XOR の値(合成信号)の全体が、すべての目標信号値 \(T_j\) を含むような設定の数を数える問題です。
考察
XOR と線形空間
「いくつかの値を選んで XOR を取る」という操作は、\(\mathrm{GF}(2)\)(\(0,1\) の二元体)上のベクトル空間で考えると見通しが良くなります。各信号値を「ビットごとに \(0/1\) を並べた \(60\) 次元ベクトル」とみなすと、XOR はベクトルの加算(足し算)に対応します。
すると、送信機 \(1,\dots,N\) の信号値を \(x_1,\dots,x_N\) としたとき、部分集合を選んで作れる合成信号の全体は、\(x_1,\dots,x_N\) が張る線形空間(span)に一致します。空集合は \(0\) に対応します。
したがって、目標 \(T_j\) を実現できることは「\(T_j\) がこの線形空間に含まれること」と同値です。
すべての目標を実現できる条件
すべての \(T_j\) が span に含まれるためには、\(T_1,\dots,T_M\) が張る空間が、\(x_1,\dots,x_N\) が張る空間に含まれていればよいです。
ここで重要なのは、\(T\) 側を基底(linear basis)にまとめてしまえることです。\(M\) 個の目標すべてを個別にチェックする必要はなく、\(T\) から作った基底の各ベクトルが送信機側の span に入るかだけ確認すれば十分です。基底に含まれるベクトルがすべて入っていれば、その線形結合である任意の \(T_j\) も自動的に入るからです。
素朴なアプローチと枝刈り
\(N \le 15\) なので、モードの割り当ては最大 \(2^{15} = 32768\) 通りです。これは全列挙できます。各割り当てについて送信機側の基底を作り、目標基底のベクトルがすべて span に入るか判定すればよいです。
ただし、\(N\) 個のベクトルが張る空間の次元は高々 \(N\) です。よって目標基底の次元(ランク)が \(N\) を超えている場合、どんな割り当てでも実現不可能なので、即座に答え \(0\) を返せます(小さな枝刈りですが正当性のためにも有効です)。
アルゴリズム
- 目標側の基底を作る: \(T_1,\dots,T_M\) から \(\mathrm{GF}(2)\) 上の線形基底
tbasisを構築する。最上位ビット位置ごとに代表ベクトルを保持する「掃き出し法」で実装する。- もし目標基底のランクが \(N\) を超えたら答えは \(0\)。
- 全割り当てを列挙: \(2^N\) 通りの
maskを試す。maskのビット \(i\) が立っていれば送信機 \(i\) はモード B(値 \(W_i\))、立っていなければモード A(値 \(V_i\))とする。 - 送信機側の基底を構築: 選ばれた \(N\) 個の値から基底
basisを作る。 - 包含判定: 目標基底の各ベクトル
tvをbasisで掃き出して \(0\) に落ちるか調べる。途中で最上位ビットに対応する基底ベクトルが存在せず \(0\) にできない場合、tvは span に含まれないので、その割り当ては不適。 - すべての目標基底ベクトルが \(0\) に落ちる割り当ての数を数え、\(10^9+7\) で割った余りを出力する。
掃き出し(基底への挿入・判定)の流れ
ベクトル \(v\) について、最上位ビット位置 \(h\) を見て、basis[h] が存在すればそれと XOR して \(v\) を小さくし、存在しなければそこに \(v\) を登録する(挿入の場合)、あるいは \(0\) にできず失敗とする(判定の場合)、という操作を \(v\) が \(0\) になるまで繰り返します。
計算量
ビット幅を \(B=60\) とします。
- 目標基底の構築: \(O(M \cdot B)\)
- 各割り当ての処理: 基底構築に \(O(N \cdot B)\)、判定に \(O(\mathrm{rank} \cdot B) \le O(N \cdot B)\)
- これを \(2^N\) 通り行うので、全体で \(O(2^N \cdot N \cdot B)\)
したがって、
- 時間計算量: \(O(2^N \cdot N \cdot B + M \cdot B)\)(\(B=60\))
- 空間計算量: \(O(B + N + M)\)
\(N \le 15\) より \(2^N \cdot N \cdot B\) は約 \(32768 \times 15 \times 60 \approx 3 \times 10^7\) 程度で、十分高速です。
実装のポイント
信号値は最大 \(2^{60}-1\) なので、基底配列のサイズは \(60\) で確保する。Python では多倍長整数をそのまま使えるため、ビット演算で素直に書ける。
最上位ビット位置は
v.bit_length() - 1で取得できる。目標側は一度基底にまとめてから判定することで、\(M\) が大きくても判定回数を「基底の次元(高々 \(N\))」に抑えられる。これが効率化の鍵。
ランクが \(N\) を超えたら即 \(0\) を返す枝刈りを入れておくと、明らかに不可能なケースを高速に処理できる。
数える対象は割り当ての個数そのものなので、最後に
% MODを取って出力すればよい(\(2^N \le 32768\) なのでオーバーフローの心配はないが、問題指定に従って剰余を取る)。ソースコード
import sys
def main():
data = sys.stdin.buffer.read().split()
idx = 0
N = int(data[idx]); idx += 1
M = int(data[idx]); idx += 1
V = [0]*N
W = [0]*N
for i in range(N):
V[i] = int(data[idx]); idx += 1
W[i] = int(data[idx]); idx += 1
T = [int(data[idx+j]) for j in range(M)]
idx += M
MOD = 10**9 + 7
# target basis
tbasis = [0]*60
trank = 0
for t in T:
v = t
while v:
h = v.bit_length() - 1
bh = tbasis[h]
if bh == 0:
tbasis[h] = v
trank += 1
break
v ^= bh
if trank > N:
print(0)
return
tvecs = [x for x in tbasis if x]
count = 0
for mask in range(1 << N):
basis = [0]*60
m = mask
for i in range(N):
v = W[i] if (m >> i) & 1 else V[i]
while v:
h = v.bit_length() - 1
bh = basis[h]
if bh == 0:
basis[h] = v
break
v ^= bh
ok = True
for tv in tvecs:
v = tv
while v:
h = v.bit_length() - 1
bh = basis[h]
if bh == 0:
ok = False
break
v ^= bh
if not ok:
break
if ok:
count += 1
print(count % MOD)
main()
この解説は claude4.8opus-high によって生成されました。