公式

E - チームの組み合わせ / Team Combinations 解説 by admin

gpt-5.5-high

概要

各グループで選んだ部分集合の XOR をそれぞれ \(X, Y\) とするとき、\(X+Y=S\) となる部分集合の組の個数を数える問題です。

ポイントは、各グループで作れる XOR 値の集合を「XOR 線形基底」で表し、同じ XOR 値を作る部分集合数をまとめて数えることです。

考察

各グループの部分集合は \(2^N\) 通りあります。
しかし \(N \leq 50\) なので、全ての部分集合を列挙すると最大で

\[ 2^{50} \]

通りとなり、到底間に合いません。

ここで重要なのは、スキル値がすべて

\[ 0 \leq A_i, B_i < 2^{20} \]

であることです。

つまり、XOR の結果も常に \(0\) 以上 \(2^{20}\) 未満です。
XOR は各ビットごとの足し算を \(2\) で割った余りとして見られるので、部分集合 XOR の集合は「\(20\) 次元以下のベクトル空間」になります。

例えば、第 \(1\) グループで作れる XOR 値の集合を考えると、

\[ c_1 A_1 \oplus c_2 A_2 \oplus \cdots \oplus c_N A_N \]

という形で表せます。
ここで \(c_i\) は、そのメンバーを選ぶなら \(1\)、選ばないなら \(0\) です。

これは XOR に関する線形結合なので、作れる XOR 値全体は XOR 線形基底によって表せます。


線形基底のランクを \(r\) とすると、作れる異なる XOR 値の個数は

\[ 2^r \]

個です。

また、元のメンバー数は \(N\) 人なので、部分集合の選び方は \(2^N\) 通りあります。
そのうち異なる XOR 値が \(2^r\) 個あるため、各 XOR 値を作る部分集合の個数は等しく

\[ 2^{N-r} \]

個になります。

これは線形写像の核の大きさが \(2^{N-r}\) であることに対応しています。

したがって、

  • 第 \(1\) グループのランクを \(r_A\)
  • 第 \(2\) グループのランクを \(r_B\)

とすると、異なる XOR 値の組 \((X,Y)\) で \(X+Y=S\) を満たすものを数え、その個数に

\[ 2^{N-r_A} \times 2^{N-r_B} \]

を掛ければ答えになります。


注意点として、条件は

\[ X \oplus Y = S \]

ではなく、

\[ X + Y = S \]

です。

ここでの \(+\) は通常の整数加算なので、XOR の線形性だけで直接処理することはできません。
そのため、各グループで作れる XOR 値を列挙し、\(X\) に対して

\[ Y = S - X \]

が第 \(2\) グループで作れるかを調べます。

アルゴリズム

まず、各グループについて XOR 線形基底を作ります。

1. XOR 線形基底を作る

各値 \(x\) を基底に追加します。

上位ビットから見て、まだそのビットを最高位ビットに持つ基底がなければ追加します。
すでにある場合は XOR して消去します。

これにより、独立な値だけが基底に残ります。

2. 作れる XOR 値を列挙する

基底が \(r\) 個あるとき、作れる XOR 値は \(2^r\) 個です。

最初に集合を

\[ \{0\} \]

としておき、基底の値 \(v\) ごとに、現在ある値すべてに対して \(v\) を XOR した値を追加します。

例えば、現在の集合が

\[ \{0, 1\} \]

で、新しい基底が \(2\) なら、

\[ 0 \oplus 2 = 2 \]

\[ 1 \oplus 2 = 3 \]

を追加して、

\[ \{0, 1, 2, 3\} \]

になります。

3. 第 \(2\) グループの XOR 値を高速に検索できるようにする

第 \(2\) グループで作れる XOR 値を列挙し、配列 in_b に記録します。

値の範囲は \(0\) 以上 \(2^{20}\) 未満なので、長さ \(2^{20}\) の配列で管理できます。

4. 条件を満たす異なる XOR 値の組を数える

第 \(1\) グループで作れる各値 \(x\) について、

\[ y = S - x \]

を計算します。

もし

\[ 0 \leq y < 2^{20} \]

かつ、第 \(2\) グループで \(y\) が作れるなら、異なる XOR 値の組 \((x,y)\) が \(1\) つ見つかります。

この個数を pair_count とします。

5. 部分集合の選び方の個数に変換する

異なる XOR 値の組ごとに、

  • 第 \(1\) グループで \(x\) を作る部分集合は \(2^{N-r_A}\) 通り
  • 第 \(2\) グループで \(y\) を作る部分集合は \(2^{N-r_B}\) 通り

あります。

よって答えは

\[ \text{pair\_count} \times 2^{N-r_A} \times 2^{N-r_B} \]

です。

まとめると、

\[ \text{ans} = \text{pair\_count} \times 2^{(N-r_A)+(N-r_B)} \]

となります。

計算量

  • 時間計算量: \(O(N \log M + 2^{r_A} + 2^{r_B} + M)\)
  • 空間計算量: \(O(2^{r_A} + 2^{r_B} + M)\)

ただし、

\[ M = 2^{20} \]

です。

また、\(r_A, r_B \leq 20\) なので、実質的には

  • 時間計算量: \(O(N \cdot 20 + 2^{20})\)
  • 空間計算量: \(O(2^{20})\)

程度で十分高速です。

実装のポイント

XOR 線形基底では、basis[b] に「最高位ビットが \(b\) の基底」を保存しています。

for (int b = LOG - 1; b >= 0; --b) {
    if (((x >> b) & 1) == 0) continue;
    if (!basis[b]) {
        basis[b] = x;
        rank++;
        return;
    }
    x ^= basis[b];
}

この処理で、すでにある基底で表せる値は追加されず、独立な値だけが追加されます。

また、答えは \(2^{63}\) 未満と保証されていますが、計算途中のシフトなどで安全に扱うため、コードでは __int128 を使っています。

__int128 ans = (__int128)pair_count << exponent;

最後に __int128 はそのまま cout できないため、専用の出力関数で十進文字列に変換して出力しています。

ソースコード

#include <bits/stdc++.h>
using namespace std;

struct LinearBasis {
    static constexpr int LOG = 20;
    int basis[LOG]{};
    int rank = 0;

    void add(int x) {
        for (int b = LOG - 1; b >= 0; --b) {
            if (((x >> b) & 1) == 0) continue;
            if (!basis[b]) {
                basis[b] = x;
                rank++;
                return;
            }
            x ^= basis[b];
        }
    }

    vector<int> enumerate() const {
        vector<int> vals;
        vals.reserve(1 << rank);
        vals.push_back(0);

        for (int b = 0; b < LOG; ++b) {
            if (!basis[b]) continue;
            int sz = vals.size();
            for (int i = 0; i < sz; ++i) {
                vals.push_back(vals[i] ^ basis[b]);
            }
        }
        return vals;
    }
};

void print_int128(__int128 x) {
    if (x == 0) {
        cout << 0 << '\n';
        return;
    }
    string s;
    while (x > 0) {
        s.push_back(char('0' + x % 10));
        x /= 10;
    }
    reverse(s.begin(), s.end());
    cout << s << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int N, S;
    cin >> N >> S;

    LinearBasis la, lb;

    for (int i = 0; i < N; ++i) {
        int a;
        cin >> a;
        la.add(a);
    }

    for (int i = 0; i < N; ++i) {
        int b;
        cin >> b;
        lb.add(b);
    }

    constexpr int M = 1 << 20;

    vector<int> avals = la.enumerate();
    vector<int> bvals = lb.enumerate();

    vector<unsigned char> in_b(M, 0);
    for (int y : bvals) in_b[y] = 1;

    long long pair_count = 0;
    for (int x : avals) {
        int y = S - x;
        if (0 <= y && y < M && in_b[y]) {
            pair_count++;
        }
    }

    int exponent = (N - la.rank) + (N - lb.rank);
    __int128 ans = (__int128)pair_count << exponent;

    print_int128(ans);

    return 0;
}

この解説は gpt-5.5-high によって生成されました。