けんちょんの競プロ精進記録

競プロの精進記録や小ネタを書いていきます

AtCoder ABC 313 D - Odd or Even (青色, 550 点)

 b + c = 5, c + a = 6, a + b = 7 みたいな連立方程式を解いた経験があれば、思いつきやすいかもしれない。

問題概要 (インタラクティブ)

0 と 1 のみからなるサイズ  N の数列  A_{1}, A_{2}, \dots, A_{N} があるが、未知である。いくつか質問を繰り返すことで、この数列を特定したい。

最大で  N 回まで質問することができる。

各質問では、 N 個の要素のうち、ちょうど  K 個の要素の index を指定する。それに対して、それら  K 個の要素の XOR 和が返される。

数列  A_{1}, A_{2}, \dots, A_{N} を特定せよ。

制約

  •  1 \le K \lt N \le 1000
  •  K は奇数

考えたこと:対称的な連立方程式

前提として、次のような連立方程式を解けるようにしておこう!

 b ^  c ^  d = 1
 a ^  c ^  d = 0
 a ^  b ^  d = 1
 a ^  b ^  c = 1

これを満たす 0-1 変数  a, b, c, d の値を求めたい。こういう対称的な連立方程式は、すべて足す (今回は XOR の意味で) ものと相場は決まっている。

ここで、 a ^  a ^  a =  a のように、奇数個の  a の XOR 和は  a になることに注意しよう。このことに注意して、上の式をすべて足すと

 a ^  b ^  c ^  d = 1

となる。再びこの式と、もとの式との XOR 和をとっていくことで、 a, b, c, d が求められる。たとえば

( a ^  b ^  c ^  d ) ^ ( b ^  c ^  d) =  a

になることに注目しよう。このことを利用すると、

 a = 1 ^ 1 = 0

と求められる。同様に  b, c, d も求められる。なお、このような解法は前例がある。ぜひ、次の問題も解いてみよう!

drken1215.hatenablog.com

これを踏まえて

これを踏まえると、今回の問題も次のように解ける。たとえば、 N = 9 K = 5 とする。

このとき、まず

  •   A_{2}, A_{3}, A_{4}, A_{5}, A_{6} の XOR 和
  •   A_{1}, A_{3}, A_{4}, A_{5}, A_{6} の XOR 和
  •   A_{1}, A_{2}, A_{4}, A_{5}, A_{6} の XOR 和
  •   A_{1}, A_{2}, A_{3}, A_{5}, A_{6} の XOR 和
  •   A_{1}, A_{2}, A_{3}, A_{4}, A_{6} の XOR 和
  •   A_{1}, A_{2}, A_{3}, A_{4}, A_{5} の XOR 和

を聞くことで、  A_{1}, A_{2}, A_{3}, A_{4}, A_{5}, A_{6} の値を求めることができる。一般に、奇数  K に対して、 A_{1}, A_{2}, \dots, A_{K+1} の値を同様に求めることができる。

これができれば、残りは簡単だ。  A_{1}, A_{2}, A_{3}, A_{4} の値が分かっているので、

  •   A_{1}, A_{2}, A_{3}, A_{4},  A_{7} の XOR 和
  •   A_{1}, A_{2}, A_{3}, A_{4},  A_{8} の XOR 和
  •  \dots
  •   A_{1}, A_{2}, A_{3}, A_{4},  A_{N} の XOR 和

を聞くことで、 A_{7}, A_{8}, \dots, A_{N} の値も特定できる。

コード

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

int main() {
    int N, K;
    cin >> N >> K;
    vector<int> A(N, -1);
    
    // 聞く
    auto ask = [&](const vector<int> &ids) -> int {
        cout << "?";
        for (auto id : ids) cout << " " << id+1;
        cout << endl;
        int res;
        cin >> res;
        return res;
    };
    
    // A[0], A[1], ..., A[K] を求める
    int sum = 0;
    vector<int> ans(N);
    for (int i = 0; i < K+1; ++i) {
        vector<int> ids;
        for (int j = 0; j < K+1; ++j) {
            if (j == i) continue;
            ids.push_back(j);
        }
        ans[i] = ask(ids);
        sum ^= ans[i];
    }
    for (int i = 0; i < K+1; ++i) A[i] = sum ^ ans[i];
     
    // A[K+1], A[K+2], ..., A[N-1] を求める
    for (int i = K+1; i < N; ++i) {
        vector<int> ids({i});
        int sub = 0;
        for (int j = 0; j < K-1; ++j) {
            sub ^= A[j];
            ids.push_back(j);
        }
        ans[i] = ask(ids);
        A[i] = sub ^ ans[i];
    }
    
    cout << "!";
    for (int i = 0; i < N; ++i) cout << " " << A[i];
    cout << endl;
}

別解

 K 要素を乱択するのを繰り返した上で、Gauss-Jordan の掃き出し法を用いることで、大体の場合解ける。