ckw1140's profile image

ckw1140

December 14, 2019 12:00

주어진 수들의 XOR 연산으로 만들 수 있는 수

주어진 수들의 XOR 연산으로 만들 수 있는 수

자연수의 집합 ${a_1, a_2, …, a_N}$ 이 주어집니다.

우리는 이 중 두 개의 원소를 골라 XOR한 결과를 집합에 추가할 수 있습니다.

이 때, 다음 문제들에 대해 생각해보겠습니다.

  1. 자연수 $b$가 주어질 때, 이 $b$가 집합의 원소가 될 수 있는지 판별하기.
  2. 이 집합에 들어갈 수 있는 원소의 최대 개수 구하기.

이 두 문제에 빠르게 답할 수 있는 방법을 알아보겠습니다.

우선, 첫번째로 어떤 수들이 집합에 추가 될 수 있는지 알아보겠습니다.

규칙에 의해 다음과 같은 꼴의 수들이 집합에 추가될 수 있습니다.

$(((a_{i_1} XOR$ $a_{i_2}) XOR$ $a_{i_3}) XOR$ $a_{i_4})$ $… XOR$ $a_{i_k}$

한편, XOR은 결합 법칙이 성립하므로 이는

$a_{i_1} XOR$ $a_{i_2} XOR$ $a_{i_3} XOR$ $a_{i_4} XOR$ $…XOR$ $a_{i_k}$

와 같이 나타낼 수 있습니다.

이제 이 식을 선형 대수학적으로 생각해 봅시다.

어떤 자연수의 이진법 표현을 GF(2) 상에서의 벡터로 해석할 수 있습니다.
예를 들어, $13 = 1101_{(2)}$ 를 [1 0 1 1]$^T$ 로 해석하는 것입니다.

그러면 XOR은 이 벡터들 사이에 더하기 연산과 같고, 위에서 구한 식은 GF(2) 상의 벡터들의 선형 결합으로 해석할 수 있습니다.

즉, 이 집합에 포함될 수 있는 벡터는 주어진 벡터들이 span하는 공간에 포함된 모든 벡터들일 것입니다.

따라서 위의 문제들은 아래와 같은 물음으로 바뀝니다.

  1. 벡터 b가 주어지면, 이 벡터가 문제에서 주어진 벡터들이 span하는 공간 위에 있는지 판별하기
  2. 문제에서 주어진 벡터들이 span하는 공간의 basis의 부분집합의 개수 구하기

문제에서 주어진 벡터들이 span 하는 공간에 대한 정보를 가장 직접적이고 간결하게 얻을 수 있는 것이 그 공간의 basis이기 때문에 basis를 구해보도록 합시다.

주어진 벡터들의 basis를 구하는 간단한 방법은 각 벡터를 row로 가지는 matrix의 row echelon form을 구하는 것입니다. 여기서 0 vector가 아닌 row vector들이 basis가 되는 것이죠.

이 때 드는 시간은 $O(Nlog^2MAXVAL)$ 입니다.
$a_i$ 의 크기가 $10^9$정도라고 해도 $logMAXVAL$이 30 정도로 작기 때문에 꽤나 효율적입니다.

이렇게 row echelon form을 통해 basis를 구했다면, 위의 문제들을 간단하게 해결할 수 있습니다.

먼저, 2번은 간단합니다. 답은 $2^{ basis }$ 가 될 것입니다.
이 값은 $O(log basis )$ 시간에 구할 수 있습니다.

또한 1번의 답도 간단하게 구할 수 있습니다.
basis들을 row로 가지는 행렬을 $A$라고 하면, $Ax=b$의 해가 존재하는지 판별하는 것이고 이는 마찬가지로 $A$의 row echelon form을 구하면 $O(log^2MAXVAL)$ 시간에 빠르게 판단할 수 있습니다.

1번과 같은 물음이 $Q$개 주어졌다고 하면, $O(QNlog^2MAXVAL)$ 시간에 답을 구할 수 있겠네요.
하지만 문제에서 주어진 벡터들이 고정인채로 $Q$개의 물음이 있는 경우에는 주어진 벡터들의 row echelon form이 고정이므로 한 번만 계산하면 됩니다.

따라서 총 $O(Nlog^2MAXVAL + Qlog^2MAXVAL)$ 시간에 구할 수 있습니다.

이 방법을 응용하면 아래와 같은 문제를 풀 수 있습니다.

[BOJ 11191 Xor Maximization]

이 문제는 위와 같은 조건에서 아래와 같은 문제에 답해야 합니다.

집합에 포함될 수 있는 원소의 최댓값을 구하기.

위에서 처럼 row echelon form을 구했다면 그리디한 방법으로 이 문제를 해결할 수 있습니다.

위에 있는 row 부터 보면 결과 벡터의 사전순서를 가장 크게 만들기 위해 이 row를 사용할지 말지를 결정할 수 있기 때문입니다.

따라서 이 문제는 $O(Nlog^2MAXVAL)$ 시간에 해결할 수 있습니다.

단, 64bit이하의 자연수에 대해 비트 연산이 O(1)에 동작하므로 아래의 코드는 $O(NlogMAXVAL)$ 시간에 동작합니다.

아래는 이 문제를 해결하는 코드입니다.

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

typedef long long ll;

const int maxn = 100010;

int N;
ll A[maxn];
vector<ll> basis;

int main() {
    scanf("%d", &N);

    for(int i = 0; i < N; i++) {
        scanf("%lld", &A[i]);
    }

    while(1) {
        ll mx = *max_element(A, A + N);
        if(!mx) break;
        basis.push_back(mx);
        for(int j = 0; j < N; j++) if((mx ^ A[j]) < A[j]) A[j] ^= mx;
    }

    ll ans = 0;
    for(int i = 0; i < basis.size(); i++) if(ans < (ans ^ basis[i])) ans ^= basis[i];
    printf("%lld", ans);
}

[BOJ 16685 XOR 포커]

이 문제는 위의 문제와 동일하지만 ‘짝수’개의 수를 사용하여야 한다는 조건이 있습니다.

우선 위와 같이 basis를 구해보도록 합시다.
각 basis 마다 원래 주어진 수들을 짝수개 사용한 것인지 홀수개 사용한 것인지 기록해 둡니다.
그 다음 위와 같이 그리디한 방법으로 최댓값을 구해봅시다.

이 때, 총 사용한 수의 개수가 짝수개라면 이 값을 출력하고 종료합니다.
만일 사용한 수의 개수가 홀수개라면 짝수개만 사용해서는 이 최댓값을 만들지 못할 수도 있습니다.
그렇기 때문에 이 경우는 잘 따져봐 주어야 합니다.

그러면 그리디한 방법으로 구한 사용한 수의 개수가 홀수개이더라도 짝수개를 사용해서 동일한 값을 만들 수 있는 경우는 어떤 경우일 까요?

바로 홀수개의 수로 0을 만들 수 있는 경우입니다.
이것이 필요충분 조건임을 쉽게 증명할 수 있습니다. 충분 조건임은 자명하고, 두 방식에 사용된 수들의 집합을 대칭차힙합한 결과 집합의 크기는 홀수이고, XOR 한 결과는 0일 것이기 때문입니다.

홀수개의 수의 XOR로 0을 만들 수 있는 지 알아보는 방법은 간단합니다. 우선 주어진 수들을 basis의 선형 결합으로 표현할 때, 원래 수를 홀수개 쓰는지 짝수개 쓰는지 기록합니다.
만일 짝수개 사용하는 경우가 있다면 이 경우는 홀수개의 수의 XOR로 0을 만들 수 있습니다.
그렇지 않다면 불가능합니다.

홀수개의 수의 XOR로 0을 만드는 것이 불가능한 경우, 즉, 짝수개의 수의 XOR로 위에서 계산한 최댓값을 만들지 못하는 경우에는 어떻게 해주어야 할까요?

이 경우에는 똑같이 그리디하게 진행하다가 basis 중 원래 수를 홀수개 사용하는 가장 작은 수의 결정을 반대로 해준다음 다시 그리디하게 진행하면 됩니다.

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

typedef long long ll;

const int maxn = 100010;

int N;
ll A[maxn];
int B[maxn], C[maxn];
vector<pair<ll, int> > basis;
ll ans;

int main() {
    scanf("%d", &N);

    for(int i = 0; i < N; i++) {
        scanf("%lld", &A[i]);
        B[i] = 1;
    }

    int la = -1;
    while(1) {
        ll mx = 0;
        int p = -1;
        for(int i = 0; i < N; i++) if(!C[i] && mx < A[i]) {
            mx = A[i];
            p = i;
        }
        if(p == -1) break;
        C[p] = 1;
        if(B[p]) la = basis.size();
        basis.push_back({ mx, B[p] });
        for(int i = 0; i < N; i++) if(!C[i] && (A[i] ^ mx) < A[i]) {
            A[i] ^= mx;
            B[i] ^= B[p];
        }
    }

    bool found = false;
    for(int i = 0; i < N; i++) if(!A[i] && B[i]) {
        found = true;
        break;
    }
    if(found) {
        for(int i = 0; i < basis.size(); i++) if(ans < (ans ^ basis[i].first)) ans ^= basis[i].first;
        printf("%lld", ans);
    }
    else {
        int cnt = 0;
        for(int i = 0; i < basis.size(); i++) if(ans < (ans ^ basis[i].first)) {
            ans ^= basis[i].first;
            cnt ^= basis[i].second;
        }
        if(cnt % 2) {
            ans = 0;
            for(int i = 0; i < basis.size(); i++) {
                if(i != la) {
                    if(ans < (ans ^ basis[i].first)) ans ^= basis[i].first;
                }
                else {
                    if(ans >= (ans ^ basis[i].first)) ans ^= basis[i].first;
                }
            }
        }
        printf("%lld", ans);
    }
}

[BOJ 16904 집합과 쿼리]

이 문제는 추가 제거 연산이 반복적으로 주어질 때, 첫번째 문제를 여러번 푸는 것입니다.
오프라인으로 문제를 해결합시다.
각 수마다 그 수가 존재하는 시점(쿼리 번호 기준)의 구간들을 미리 구해 놓을 수 있습니다.
구간의 개수는 $O(Q)$ 개 일것입니다.

이제 $[1, Q]$ 의 시점들에 대한 분할 정복을 사용하여 이 문제를 해결할 수 있습니다.
분할 정복의 순서를 왼쪽 구간에서 오른쪽 구간을 처리하는 방식으로 잘 정해주면, 자신의 자식 구간에 대한 재귀를 호출할 때, 그 시점의 구간에서 전부 존재하는 수들에 대한 basis를 빠르게 갱신해 나갈 수 있습니다.
따라서 아래와 같은 코드로 해결 가능합니다.
시간 복잡도는 $O(QlogQlogMAXVAL)$ 입니다.

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

int Q;
map<int, int> chk;
int V[31];

void store(int *tmp) {
    for(int i = 0; i < 31; i++) tmp[i] = V[i];
}
void restore(int *tmp) {
    for(int i = 0; i < 31; i++) V[i] = tmp[i];
}
void add(int v) {
    for(int i = 30; i >= 0; i--) {
        if(v & (1 << i)) {
            if(V[i]) v ^= V[i];
            else {
                V[i] = v;
                break;
            }
        }
    }
}
int getMax() {
    int ret = 0;
    for(int i = 30; i >= 0; i--) if(ret < (ret ^ V[i])) ret ^= V[i];
    return ret;
}

struct BIT {
    vector<vector<int> > tree;
    void init() {
        tree = vector<vector<int> >(4 * Q);
    }
    void upd(int a, int b, int v, int l, int r, int n) {
        if(b < l || r < a) return;
        if(a <= l && r <= b) {
            tree[n].push_back(v);
            return;
        }
        int m = (l + r)>>1;
        upd(a, b, v, l, m, 2*n);
        upd(a, b, v, m + 1, r, 2*n + 1);
    }
    void dfs(int l, int r, int n) {
        int tmp[31];
        store(tmp);
        for(int i = 0; i < tree[n].size(); i++) {
            add(tree[n][i]);
        }
        if(l == r) {
            cout << getMax() << '\n';
            restore(tmp);
            return;
        }
        int m = (l + r)>>1;
        dfs(l, m, 2*n);
        dfs(m + 1, r, 2*n + 1);
        restore(tmp);
    }
} bit;

int main() {
    std::ios::sync_with_stdio(false);
    cin >> Q;

    bit.init();
    for(int i = 0; i < Q; i++) {
        int x; cin >> x;
        x = abs(x);

        if(chk[x]) {
            bit.upd(chk[x] - 1, i - 1, x, 0, Q - 1, 1);
            chk[x] = 0;
        }
        else chk[x] = i + 1;
    }
    for(auto it = chk.begin(); it != chk.end(); it++) {
        if(it->second) {
            bit.upd(it->second - 1, Q - 1, it->first, 0, Q - 1, 1);
        }
    }
    bit.dfs(0, Q - 1, 1);
}