이 포스팅은 Algorithm Problem Solving 시리즈 113 편 중 54 번째 글 입니다.

  • Part 1 - 백준(1003번): 피보나치 함수
  • Part 2 - 백준(1010번): 다리놓기
  • Part 3 - 백준(1012번): 유기농 배추
  • Part 4 - 백준(10159번): 저울
  • Part 5 - 백준(10164번): 격자상의 경로
  • Part 6 - 백준(1018번): 체스판 다시 칠하기
  • Part 7 - 백준(1018번): 체스판 다시 칠하기
  • Part 8 - 백준(1022번): 소용돌이 예쁘게 출력하기
  • Part 9 - 백준(10775번): 공항
  • Part 10 - 백준(10816번): 숫자 카드2
  • Part 11 - 백준(10816번): 숫자카드 2
  • Part 12 - 백준(10819번): 차이를 최대로
  • Part 13 - 백준(10830번): 행렬 곱셈
  • Part 14 - 백준(10844번): 쉬운 계단수
  • Part 15 - 백준(10868번): 최소값
  • Part 16 - 백준(1092번): 배
  • Part 17 - 백준(11003번): 최솟값 찾기
  • Part 18 - 백준(1102번): 발전소
  • Part 19 - 백준(11048번): 이동하기
  • Part 20 - 백준(11053번): 가장 긴 증가하는 부분 수열
  • Part 21 - 백준(11054번): 가장 긴 바이토닉 부분 수열
  • Part 22 - 백준(11055번): 가장 긴 증가하는 부분 수열2
  • Part 23 - 백준(11057번): 오르막 수
  • Part 24 - 백준(1120번): 문자열
  • Part 25 - 백준(11403번): 경로 찾기
  • Part 26 - 백준(11404번): 플루이드
  • Part 27 - 백준(1149번): RGB 거리
  • Part 28 - 백준(11559번): puyo puyo
  • Part 29 - 백준(11655번): ROT13
  • Part 30 - 백준(1167번): 트리의 지름
  • Part 31 - 백준(11722번): 가장 감소하는 부분 수열
  • Part 32 - 백준(12015번): 가장 긴 증가하는 부분 수열(LIS) 2
  • Part 33 - 백준(12851번): 숨바꼭질2
  • Part 34 - 백준(12852번): 1로 만들기 2
  • Part 35 - 백준(12865번): 평범한 배낭
  • Part 36 - 백준(1300번): K번째 수
  • Part 37 - 백준(1325번): 효율적인 해킹
  • Part 38 - 백준(13549번): 숨바꼭질3
  • Part 39 - 백준(13913번): 숨바꼭질4
  • Part 40 - 백준(14002번): 가장 긴 증가하는 부분 수열 4
  • Part 41 - 백준(1431번): 시리얼 넘버
  • Part 42 - 백준(1436번): 영화감독 숌
  • Part 43 - 백준(14499번): 주사위 굴리기
  • Part 44 - 백준(14888번): 연산자 끼워넣기
  • Part 45 - 백준(14889번): 스타트와 링크
  • Part 46 - 백준(14891번): 톱니바퀴
  • Part 47 - 백준(15658번): 연산자 끼워넣기 2
  • Part 48 - 백준(15686번): 치킨 배달
  • Part 49 - 백준(1697번): 숨바꼭질
  • Part 50 - 백준(1697번): 숨바꼭질
  • Part 51 - 백준(1707번): 이분 그래프(Bipartite Graph)
  • Part 52 - 백준(1708번): 볼록 껍질
  • Part 53 - 백준(17136번): 색종이 붙이기
  • Part 54 - This Post
  • Part 55 - 백준(17298번): 오큰수
  • Part 56 - 백준(17626번): Four Squares
  • Part 57 - 백준(18870번): 좌표 압축
  • Part 58 - 백준(1890번): 점프
  • Part 59 - 백준(1918번): 후위 표기식
  • Part 60 - 백준(1929번): 소수 구하기
  • Part 61 - 백준(1963번): 소수 경로
  • Part 62 - 백준(1965번): 상자넣기
  • Part 63 - 백준(1976번): 여행가자
  • Part 64 - 백준(1987번): 알파벳
  • Part 65 - 백준(1992번): 쿼드트리
  • Part 66 - 백준(2003번): 수들의 합2
  • Part 67 - 백준(20040번): 사이클 게임
  • Part 68 - 백준(2042번): 구간 합 구하기
  • Part 69 - 백준(2108번): 통계학
  • Part 70 - 백준(2110번): 공유기 설치
  • Part 71 - 백준(2156번): 포도주 시식
  • Part 72 - 백준(2193번): 이친수
  • Part 73 - 백준(2231번): 분해합
  • Part 74 - 백준(2250번): 트리의 높이와 너비
  • Part 75 - 백준(2293번): 동전 1
  • Part 76 - 백준(2294번): 동전 2
  • Part 77 - 백준(2343번): 기타 레슨
  • Part 78 - 백준(2468번): 안전 영역
  • Part 79 - 백준(2512번): 예산
  • Part 80 - 백준(2529번): 부등호
  • Part 81 - 백준(2565번): 전깃줄
  • Part 82 - 백준(2581번): 소수
  • Part 83 - 백준(2583번): 영역구하기
  • Part 84 - 백준(2630번): 색종이 만들기
  • Part 85 - 백준(2644번): 촌수계산
  • Part 86 - 백준(2667번): 단지번호붙이기
  • Part 87 - 백준(2751번): 수 정렬하기 2
  • Part 88 - 백준(2798번): 블랙잭
  • Part 89 - 백준(2904번): 수학은 너무 쉬워
  • Part 90 - 백준(2986번): 파스칼
  • Part 91 - 백준(3015번): 오아시스 재결합
  • Part 92 - 백준(3190번): 뱀
  • Part 93 - 백준(3190번): 벽 부수고 이동하기
  • Part 94 - 백준(4195번): 친구 네트워크
  • Part 95 - 백준(4948번): 베르트랑 공준
  • Part 96 - 백준(4963번): 섬의 개수
  • Part 97 - 백준(4991번): 로봇 청소기
  • Part 98 - 백준(5373번): 큐빙
  • Part 99 - 백준(5437번): 불
  • Part 100 - 백준(6171번): 땅따먹기
  • Part 101 - 백준(6603번): 로또
  • Part 102 - 백준(6603번): 로또
  • Part 103 - 백준(7562번): 나이트의 이동
  • Part 104 - 백준(7568번): 덩치
  • Part 105 - 백준(7576번): 토마토
  • Part 106 - 백준(7579번): 앱
  • Part 107 - 백준(7785번): 회사에 있는 사람
  • Part 108 - 백준(9012번): 괄호
  • Part 109 - 백준(9020번): 골드바흐의 추측
  • Part 110 - 백준(9184번): 신나는 함수 실행
  • Part 111 - 백준(9251번): LCS
  • Part 112 - 백준(9421번): 소수상근수
  • Part 113 - 프로그래머스: 가장 큰 정사각형 찾기
▼ 목록 보기

골드4 : 유니온 파인드 문제이다.

생각

문제를 이해해보자. 먼저 처음에 아무일도 하지 않을 경우에 입력된 n에 대해서 0 ~ n까지의 바구니가 생긴다. 그 다음, m개의 명령이 들어온다. 명령은 해당 원소가 들어있는 바구니를 합치는 것, 그리고 해당 원소가 같은 바구니에 들어있는 지 확인하는 것이다.

그렇다면 핵심은, 현재 어떻게 내가 들어가 있는 바구니를 찾을 것인가? 그리고 찾았다면 어떤식으로 합쳐서 가지고 있을 것인가? 이다. 결국 집합을 만들고, 서로소 인지를 확인하는 문제이다.

가장 단순하게 생각해보자. 처음에 모든 바구니는 자기자신만을 원소로 갖는 바구니를 가진다. 이 상태에서 1번 바구니와 3번 바구니를 합치라는 명령이 내려올 때, 우리는 일반적으로 이것을 동등하게 생각하고 연결하려 한다. 하지만 이렇게 생각할 경우에 해당 바구니에 대한 번호를 재정의 해야 한다. 예를 들어, (1, 2)가 들어있는 바구니는 몇번 바구니라 정의해야 할까? 또 추가적으로 3이 들어온 경우 (1, 2, 3)은 몇번 바구니 일까? 바구니에 원소가 추가될 경우 계속하여 바구니의 이름을 재정의 해야 한다.

그러니 위계 질서를 줘보자. 1번 바구니와 2번 바구니를 합친다는 명령이 내려왔을 때 2번 바구니는 1번을 따른다. (2->1) 그리고 3번 바구니는 2번을 따른다고 하면 (3->2->1)로 준다. 그리고 이렇게 생성된 바구니의 번호를 1이라 하면, 합치는 과정을 통한다하더라도 합쳐진 집합의 번호는 가장 먼저 합쳐진, 즉 조상 원소의 번호라 지칭할 수 있다. 즉, 위의 바구니는 1번 바구니라 할 수 있고, 다른 합쳐진 집합이 있다하더라도 이 집합의 조상은 독립적으로 유지된다.

(3->2->1) = 1번 바구니
(4->5) = 5번 바구니

5번 바구니와 3번 바구니를 합해라.
=> (4->5->3->2->1) = 1번 바구니

문제 발생

하지만 이 경우에는 문제가 발생하는데, (2->1), (5->4) 인 경우가 있다고 하자. 이 때 5번이 2번 바구니에 속한다고 할 경우 이 논리대로라면 (5->2->1)이 된다. 그런데 사실은 4번도 1번 바구니에 속해야 한다. 논리적 오류가 발생한다.

이 문제는 바구니가 속하는 기준이 한 방향이기 때문이다. 하지만 속하는 기준을 여러개로 만들지 않아도 해결이 가능하다.

해결 방법

아까 조상을 만들고, 추가된 집합에 대한 바구니 번호를 가장 위에 있는 조상의 번호를 따르기로 했다. 위에서 발생한 문제에 대해 해당 번호가 어떤 바구니(조상)에 있는지 확인하고 그 바구니끼리 연결한다면 해당 문제는 해결된다.

(2->1) = 1번 바구니
(5->4) = 4번 바구니

5번 바구니와 2번 바구니를 합쳐라.

5번의 조상 : 4번
2번의 조상 : 1번

(4->1)로 바꿔버린다.

=> (2->1), (5->4->1)

이렇게 된 경우 1번 바구니에 해당한 번호는 2, 4, 5로 원하는 결론이다.

구현

이것을 구현하는 것이 어려워 보이지만, 단순하다. 위에서 결국 필요한 것은 내 원소의 번호, 그리고 그 원소가 따르는 번호이다. 그리고 그 따르는 것들의 바구니 번호는 가장 위에 기초가 되는 조상 번호가 대표한다.

바구니 0 1 2 3 4 5 6 7
번호 0 1 2 3 4 5 6 7
7 8
0 1 3
1 1 7
0 7 6
1 7 1
0 3 7
0 4 2
0 1 1
1 1 1

line 1

0 1 3 // 1번과 3번을 합쳐라.
바구니 0 1 2 3 4 5 6 7
번호 0 1 2 3 4 5 6 7

1의 조상 번호 : 1
3의 조상 번호 : 3
3번이 따르는 바구니를 1로 바꾼다.

바구니 0 1 2 3 4 5 6 7
번호 0 1 2 1 4 5 6 7

line 2

1 1 7 // 1번과 7번은 같은 바구니이니?
바구니 0 1 2 3 4 5 6 7
번호 0 1 2 1 4 5 6 7

7의 조상 번호 : 7
1의 조상 번호 : 1

No

line 3

0 7 6 // 7번과 6번을 합쳐라.
바구니 0 1 2 3 4 5 6 7
번호 0 1 2 1 4 5 6 7

7의 조상 번호 : 7
6의 조상 번호 : 6
6번은 7번을 따른다.

바구니 0 1 2 3 4 5 6 7
번호 0 1 2 1 4 5 7 7

line 4

1 7 1 // 7번과 1번은 같은 바구니이니?
바구니 0 1 2 3 4 5 6 7
번호 0 1 2 1 4 5 7 7

7의 조상 번호 : 7
1의 조상 번호 : 1

No

line 5

0 3 7 // 3번과 7번을 합쳐라.
바구니 0 1 2 3 4 5 6 7
번호 0 1 2 1 4 5 7 7

3의 조상 번호 : 1
7의 조상 번호 : 7
7번은 3번을 따른다.

바구니 0 1 2 3 4 5 6 7
번호 0 1 2 1 4 5 7 3

line 6

0 4 2 // 4번과 2번을 합쳐라.
바구니 0 1 2 3 4 5 6 7
번호 0 1 2 1 4 5 7 3

4의 조상 번호 : 4
2의 조상 번호 : 2
2번은 4번을 따른다.

바구니 0 1 2 3 4 5 6 7
번호 0 1 4 1 4 5 7 3

line 7

0 1 1 // 1번과 1번을 합쳐라.
바구니 0 1 2 3 4 5 6 7
번호 0 1 2 1 4 5 7 3

1의 조상 번호 : 1
1의 조상 번호 : 1
1번은 1번을 따른다.

바구니 0 1 2 3 4 5 6 7
번호 0 1 4 1 4 5 7 3

line 8

1 1 1 // 1번과 1번은 같은 바구니이니?
바구니 0 1 2 3 4 5 6 7
번호 0 1 4 1 4 5 7 3

1의 조상 번호 : 1
1의 조상 번호 : 1

Yes

결론

이런 방식으로 업데이트 하는 과정을 거치면, 묶인 집합을 효과적으로 찾을 수 있다. 결론적으로 구현해야 하는 함수는 find, union 함수이다.

find

find 함수같은 경우에는, 특정 원소에 대해 물었을 때, 그 녀석이 속한 바구니를 끝까지 찾아 어떤 바구니인지 반환한다.

union

union 함수는 명령이 들어왔을 때, 각각이 속해있는 바구니를 찾고 그 바구니를 연결한다. 이 때, 중요한 것은 해당 원소의 바구니 번호를 찾아야 한다는 것이다.

#include <iostream>
#include <algorithm>
#include <string>
#include <vector>
using namespace std;
int n, m;
int box[1000001];

int find(int elem){
    if (elem == box[elem]) {
        return elem;
    }
    return find(box[elem]);
}
void merge(int elem1, int elem2){
    box[find(elem2)] = find(elem1);
}

int main(){
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    cin >> n >> m;
    for (int i = 0; i <= n; i++) {
        box[i] = i;
    }
    for (int i = 0; i < m; i++) {
        int command, elem1, elem2;
        cin >> command >> elem1 >> elem2;
        if (command) {
            if (find(elem1) == find(elem2)) {
                cout << "YES" << '\n';
            } else {
                cout << "NO" << '\n';
            }
        } else {
            merge(elem1, elem2);
        }
    }
    return 0;
}

시간 복잡도 문제

하지만 위의 코드대로 한다면 문제가 발생한다. 그 이유는 원소의 개수가 많아짐에 따라 어떤 바구니에 들어있는지 탐색하는 시간이 오래걸리기 때문이다. 예제 입력에 대한 마지막 상황을 보자.

바구니번호 0 1 2 3 4 5 6 7
따르는 번호 0 1 4 1 4 5 7 3

이 상황에서, 6은 어느 바구니에 있을까? (6->7->3->1) 로, 1번 바구니에 있다. 만약, 6번을 0번이 속한 바구니와 합치라는 명령이 들어온다면 4번의 연산을 수행해야 한다. 그렇다면, 극단적인 경우에, n개의 원소가 순차적으로 얽혀있고 (n->n-1->n-2->…) n이라는 원소를 특정 바구니에 합치라는 명령이 들어올 경우, $O(n)$ 의 시간 복잡도를 갖는다. 이 때, 수행하는 명령의 개수가 m일 경우 $O(nm)$ 의 시간 복잡도를 갖는다. 현재 문제에서 n = 100000, m = 1000000이므로 시간 초과가 난다.

해결

이 문제는 해결이 간단한데, find 함수에 찾는 과정에서, 최종 조상의 번호를 아예 해당 원소의 바구니라고 저장해버리는 것이다. 이 상황에서는 원소가 속해있는 바구니 번호가 중요한 것이지, 그 과정에서 발생하는 그래프가 중요한 것이 아니기 때문이다.

#include <iostream>
#include <algorithm>
#include <string>
#include <vector>
using namespace std;
int n, m;
int box[1000001];

int find(int elem){
    if (elem == box[elem]) {
        return elem;
    }
    return box[elem] = find(box[elem]);
}
void merge(int elem1, int elem2){
    box[find(elem2)] = find(elem1);
}

int main(){
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    cin >> n >> m;
    for (int i = 0; i <= n; i++) {
        box[i] = i;
    }
    for (int i = 0; i < m; i++) {
        int command, elem1, elem2;
        cin >> command >> elem1 >> elem2;
        if (command) {
            if (find(elem1) == find(elem2)) {
                cout << "YES" << '\n';
            } else {
                cout << "NO" << '\n';
            }
        } else {
            merge(elem1, elem2);
        }
    }
    return 0;
}

Reference

백준(1717번) - 집합의 표현