Official

E - 通信モードの切り替え / Switching Communication Modes Editorial by admin

gemini-3.5-flash-high

概要

各送信機の動作モード(AまたはB)を決定したときに、目標とするすべての信号値が「選んだ送信機の信号値の XOR(排他的論理和)の組み合わせ」によって作り出せるかを判定する問題です。

送信機の数 \(N\) が最大でも \(15\) と非常に小さいことに着目し、全探索XORの線形基底(掃き出し法)を組み合わせることで効率的に解くことができます。


考察

1. 「XORで作れるか」をどう判定するか?

「いくつかの値の XOR 和によって特定の目標値を作れるか」という問題は、XORの線形基底(XOR Basis)を用いることで高速に判定できます。

これは、線形代数における「ベクトルの一次結合で表せるか(掃き出し法)」と全く同じ考え方です。 各信号値を \(2\) 進数で表したとき、各ビットを成分とするベクトルとみなすことができます。XOR 演算は、繰り上がりのない加算(GF(2) 上の加算)に対応します。

したがって、以下の手順で判定が可能になります: 1. 目標信号値の集合 \(T = \{T_1, T_2, \ldots, T_M\}\) から、それらを構成する「基底」の集合 \(B_T\) を求めます。 2. 各送信機で選んだ信号値の集合 \(X = \{x_1, x_2, \ldots, x_N\}\) から、同様に基底の集合 \(B_X\) を求めます。 3. \(T\) のすべての要素が \(X\) の要素の XOR 和で作れるための必要十分条件は、\(B_T\) のすべての要素が \(B_X\) の要素の XOR 和で表せること」です。

2. \(N \le 15\) という制約の活用

各送信機について、モード A とモード B の \(2\) 通りの選択肢があります。 送信機の総数は \(N \le 15\) なので、モードの割り当て方は最大でも \(2^{15} = 32,768\) 通りしかありません。

この数は非常に小さいため、すべての割り当て方法(\(2^N\) 通り)をビット全探索し、それぞれについて条件を満たすかを判定すれば、実行時間制限に余裕で間に合います。


アルゴリズム

ステップ 1: 目標信号値 \(T\) の基底 \(B_T\) を求める

まず、目標信号値のリスト \(T\) から基底 \(B_T\) を構築します。 もし \(B_T\) のサイズ(基底の要素数)が \(N\) より大きくなった場合、送信機の数 \(N\) よりも独立な目標値の数が多いため、どのようにモードを選んでも絶対にすべての目標値を達成できません。この場合は即座に 0 を出力して終了します。

ステップ 2: モードの割り当てを全探索

\(0\) から \(2^N - 1\) までの整数 mask をループします。 mask\(i\) 番目のビットが 0 ならば送信機 \(i\) はモード A(\(V_i\))を、1 ならばモード B(\(W_i\))を選択するものとします。

ステップ 3: 選択された信号値 \(X\) の基底 \(B_X\) を求める

mask について、選ばれた \(N\) 個の信号値から基底 \(B_X\) を構築します。

ステップ 4: 判定

\(B_T\) の各要素 \(t\) について、基底 \(B_X\) を使って掃き出しを行います。 具体的には、\(B_X\) の要素 \(b\) を用いて、\(t \oplus b < t\) となる場合に \(t \leftarrow t \oplus b\) と更新していきます。 最終的に \(t\)\(0\) になれば、その \(t\)\(B_X\) の要素の XOR 和で表せる(=作ることができる)ということです。 \(B_T\) のすべての要素が \(0\) になった場合のみ、その mask は条件を満たすため、答えのカウンタを \(+1\) します。


計算量

値の最大ビット数を \(D = 60\) とします。

  • 時間計算量: \(O(M \cdot D + 2^N \cdot N \cdot D)\)

    • \(T\) の基底を求めるのに \(O(M \cdot D)\)
    • 各マスク(\(2^N\) 通り)において、基底 \(B_X\) の構築に \(O(N \cdot D)\)、判定に \(O(|B_T| \cdot D) = O(N \cdot D)\) かかります。
    • \(N = 15, M = 100, D = 60\) を代入すると、全体の最悪演算回数は約 \(32,768 \times 15 \times 60 \approx 3 \times 10^7\) 回となり、Pythonでも \(1\) 秒未満で高速に動作します。
  • 空間計算量: \(O(N + M)\)

    • 基底のサイズは最大でも \(D\)(または \(N\))であるため、非常に少ないメモリで動作します。

実装のポイント

  • 基底の構築方法: 基底を追加する際は、値を大きい順(最上位ビットが立っている順)にソートして保持しておくと、掃き出しが簡単に行えます。 新しく要素 \(x\) を挿入する際は、既存の基底 \(b\) と比較し、\(x \oplus b < x\) となる(\(x\) の最上位ビットを消せる)場合に \(x \leftarrow x \oplus b\) と更新します。最終的に \(x > 0\) であれば、適切な位置(降順を維持する位置)に挿入します。

    ソースコード

import sys

def solve():
    input = sys.stdin.read
    data = input().split()
    if not data:
        return
    
    N = int(data[0])
    M = int(data[1])
    
    pairs = []
    idx = 2
    for _ in range(N):
        v = int(data[idx])
        w = int(data[idx+1])
        pairs.append((v, w))
        idx += 2
        
    T = [int(x) for x in data[idx:idx+M]]
    
    # T の基底を求める
    basis_T = []
    for t in T:
        for b in basis_T:
            if (t ^ b) < t:
                t ^= b
        if t > 0:
            inserted = False
            for i in range(len(basis_T)):
                if t > basis_T[i]:
                    basis_T.insert(i, t)
                    inserted = True
                    break
            if not inserted:
                basis_T.append(t)
                
    if len(basis_T) > N:
        print(0)
        return
        
    ans = 0
    pow2 = [1 << i for i in range(N)]
    
    for mask in range(1 << N):
        basis_X = []
        for i, (v, w) in enumerate(pairs):
            x = w if (mask & pow2[i]) else v
            for b in basis_X:
                if (x ^ b) < x:
                    x ^= b
            if x > 0:
                inserted = False
                for j in range(len(basis_X)):
                    if x > basis_X[j]:
                        basis_X.insert(j, x)
                        inserted = True
                        break
                if not inserted:
                    basis_X.append(x)
                    
        possible = True
        for t in basis_T:
            for b in basis_X:
                if (t ^ b) < t:
                    t ^= b
            if t > 0:
                possible = False
                break
        if possible:
            ans += 1
            
    print(ans % 1000000007)

if __name__ == '__main__':
    solve()

この解説は gemini-3.5-flash-high によって生成されました。

posted:
last update: