포스트

[종만북] 문제: EDITORWARS

서론

문제 설명

어떤 임의의 원소가 같다다르다라는 정보가 $m(1 \leq m \leq 100000)$개 제공되고, 원소의 개수는 $n(1 \leq n \leq 10000)$개 입니다.

원소의 상태가 2개라면, 같은 원소의 최대 크기를 구하시오, 만약 입력에 모순이 존재한다면, 모순이 존재함을 출력하시오.

문제 링크

문제 유형

이분 그래프 판별(bipartite graph check)문제를 풀어봅시다.

bipartite union-find 방법을 이용하여 풀 수 있습니다.

너무 어려워

Union-Find 기법과 이분 그래프 문제를 합친 문제여서 난이도가 좀 있습니다.

그래서 이걸 자력으로 풀진 못했고, 교과서 코드를 리뷰하겠습니다.

아이디어

원소의 상태가 2개기 때문에, 다음과 같은 사실을 얻을 수 있습니다.

상태를 $State()$로 정의하겠습니다.

  1. $State(a) = State(b), State(b) \neq State(c) \Rightarrow State(a) \neq State(c)$
  2. $State(a) \neq State(b), State(b) \neq State(c) \Rightarrow State(a) = State(c)$

이 내용을 정리하면, 각 집합마다 적대 관계인 집합을 얻을 수 있다는 것입니다.

이는 이분 그래프(bipartite graph)의 기본 구조와 동일합니다.

bipartite Union-find

근데 왜 갑자기 union-find기법을 사용해야 할까요?

왜냐하면, 각 원소가 어느 집합에 속하는지(상태가 같은지, 다른지)를 빠르게 판단할 수 있는 대표적인 방법이 Union-find기법인 것입니다.

그러면 이분 그래프에 Union-find기법을 적용시켜봅시다.

일반적인 Union-find처럼 단순히 집합을 합치는 것 외에,

bipartite Union-find는 각 원소가 두 가지 그룹 중 어디에 속하는지 알려주는 정보를 관리해야 합니다.

생각을 정리해보면, 우리는 정보를 저장할 배열을 몇 개 선언할 수 있습니다.

아래 3개는 일반적인 Union-Find를 쓰기 위한 배열입니다.

$parent[x]$: 노드 $x$의 부모 노드입니다.

$rank[x]$: 노드 $x$의 $rank$입니다. merge 연산에 쓰임

$size[x]$: 노드 $x$의 $size$(루트 포함 노드의 개수)입니다.

우리는 여기서 추가적인 배열을 선언해야 합니다.

$enemy[x]$: $x$가 루트라면, 해당 집합과 반대인 집합의 루트 번호입니다. (없으면 -1)

이후에는, 앞서 설명한 상태를 이용해

나와 다른 상태의 원소와 다른 상태의 원소는 나랑 같은 원소, 나와 같은 상태의 원소와 다른 상태의 원소는 나랑 다른 원소를 이용해 bipartite Union-find를 구현하면 되겠습니다.

이때! enemy[]가 쓰입니다.


알고리즘 구상

알고리즘 설계

  1. 먼저 Union-Find를 쓰기 위해 배열들을 전처리합니다.
  2. 친구 관계와 적대 관계에 대해 Union-find를 합니다. 모순이 있다면 모순을 발견합니다.
  3. 이후 부모 원소에 대해 부모와 상태가 같은 원소, 다른 원소의 크기의 최댓값을 결과에 더합니다.
  4. 이렇게 n개의 원소에 대해 반복하며, 나올 수 있는 최댓값을 출력합니다.

시간 복잡도

전처리하는 함수 Init()의 시간 복잡도는 $O(n)$.

모든 노드를 순회하며, 나올 수 있는 최대 원소의 수를 세어서 시간 복잡도는 $O(n)$.

$merge()$와 $find()$의 시간 복잡도는 $O(α(n))$, 거의 $O(1)$이라 볼 수 있기에(엄밀하진 않습니다)

입력이 $m$개가 주어져 이것에 대한 연산 처리 $m \times O(1) = O(m)$으로

총 시간 복잡도는 $O(n + m)$입니다.


알고리즘 구현

Union-find의 기본 함수

  1. init(), Union-find를 구현할 때 필요한 배열들을 전처리합니다.
1
2
3
4
5
6
7
8
void init() {
    parent.clear();
    parent.resize(n);
    iota(parent.begin(), parent.end(), 0);
    r.assign(n, 0);
    enemy.assign(n, -1);
    s.assign(n, 1);
}
  1. find(), 각 원소의 부모를 찾고, 경로 압축을 합니다.
1
2
3
4
int find(int u) {
    if (parent[u] == u) return u;
    return parent[u] = find(parent[u]);
}
  1. merge, 두 원소를 rank[]를 기준으로 합병합니다.

특이하게, 원소 2개가 없을 경우를 대비하여 이 코드를 작성합니다.

1
if (u == -1 || v == -1) return max(u, v);

이 코드는 나중에 ack()를 구현할 때 필요합니다!

1
2
3
4
5
6
7
8
9
10
int merge(int u, int v) {
    if (u == -1 || v == -1) return max(u, v);
    u = find(u), v = find(v);
    if (u == v) return u;
    if (r[u] > r[v]) swap(u, v);
    if (r[u] == r[v]) r[v]++;
    parent[u] = v;
    s[v] += s[u];
    return v;
}

친구, 적대 관계 파악 함수

dis(), 친구 관계인 두 노드를 합병하고 하고, 이로 인해 생긴 적대 관계도 관리하는 함수입니다.

만약 관계의 모순이 발생하면, false를 리턴합니다.

1
2
3
4
5
6
7
8
bool ack(int u, int v) {
    u = find(u), v = find(v);
    if (enemy[u] == v) return false;
    int a = merge(u, v), b = merge(enemy[u], enemy[v]);
    enemy[a] = b;
    if (b != -1) enemy[b] = a;
    return true;
}

dis(), 적대 관계인 두 노드를 합병하고, 이로 인해 생긴 적대 관계도 관리하는 함수입니다.

만약 관계의 모순이 발생하면, false를 리턴합니다.

1
2
3
4
5
6
7
8
bool dis(int u, int v) {
    u = find(u), v = find(v);
    if (u == v) return false;
    int a = merge(u, enemy[v]), b = merge(enemy[u], v);
    enemy[a] = b;
    enemy[b] = a;
    return true;
}

main()

앞서 구현한 Union-find를 구현합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int main() {
    fastIO;
    int C;
    cin >> C;
    while (C--) {
        cin >> n >> m;
        init();
        string cache;
        int a, b, failedIndex = -1;
        bool fail = true;
        for (int i = 1; i <= m; i++) {
            cin >> cache >> a >> b;
            if (cache[0] == 'A')
                fail = ack(a, b);
            else
                fail = dis(a, b);
            if (!fail && failedIndex == -1) failedIndex = i;
        }
        int ret = 0;
        for (int i = 0; i < n; i++) {
            if (parent[i] == i) {
                int e = enemy[i];
                if (e > i) continue;
                int mySize = s[i];
                int enemySize = e == -1 ? 0 : s[e];
                ret += max(mySize, enemySize);
            }
        }
        if (failedIndex != -1)
            cout << "CONTRADICTION AT " << failedIndex << "\n";
        else
            cout << "MAX PARTY SIZE IS " << ret << "\n";
    }
    return 0;
}

최종 코드

여기에서 확인하세요!

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.