1. 디스조인트-셋 자료구조란?


disjoint-set / union-find / merge-find 등등으로 불린다.


이름만 봐도 눈치깔 수 있드시, 서로소(disjoint)인 집합들을 가지고 합칠 수도 있고(union, merge) 원소를 찾는 등의

집합질 특화된 자료구조다.


우리 위키를 참조하자면, 이 자료구조는 여러 서로소(원소가 중복되지 않음) 부분집합들로 이루어지고,


새로운 집합을 추가하거나, 기존의 집합과 합치거나, 집합에 어떤 원소가 들어가 있는지를 찾는 연산을


거의 선형시간에 가깝게 수행하는 자료구조다!



2. 어디에 적용시킬까?


당연히 집합과 관련된(파티션질이 필요한) 문제에 써야한다.


우리는 이미 집합연산을 제공하는 stl을 알고 있다 set헤더에서 제공하는 set자료구조가 그것인데,


이 set 컨테이너는 정말 집합처럼 작동한다.


집합의 크기, 어떤 원소를 갖고 있는지 아닌지, 집합끼리의 비교도 가능하다.


이 컨테이너는 노드 기반 컨테이너이며 균형 이진트리로 구축된다.

따라서 찾기연산과 삽입연산은 로그 시간의 복잡도를 가진다고 한다.


디스조인트-셋 자료구조의 강점은 set 컨테이너보다 빠른 시간에 있다.


하지만 세상에 공짜는 없는법, 이 자료구조도 특성상 몇가지 결점이 있다.


작동원리를 세부적으로 보면서 알아보자.


1) makeset 연산


새로운 한 원소를 대표로 하는 집합을 만드는 연산이다.

function MakeSet(x) add x to the disjoint-set tree x.parent := x

parent는 x의 부모로, 계속해서 parent를 찾아가서 x가 속한 집합의 대표를 찾는데 사용한다.


2) find 연산


이 연산은 x를 포함하는 집합의 대표원소를 찾는 연산이다.


function Find(x)
   if x.parent != x
     x.parent := Find(x.parent)
   return x.parent

x부터 시작하여 계속 parent를 찾아 재귀적으로 다시 find를 호출하는 모습을 볼 수 있다.

마지막엔 자기자신을 부모로 갖고있는 원소가 리턴되는데 이 원소가 바로 집합의 대표원소다.


만약 집합 {1}과 {3}이 있었는데, 전자를 후자에 합쳤다고 해보자. {3, {1}} 그러면 이 집합의 대표는 3이다.

find(1) = 3. find(3) = 3.


3) union 연산


합집합을 담당하는 연산이다.


두 원소를 매개변수로 하고, 각각의 대표들을 찾아 하나로 통일하는 과정을 거친다.


function Union(x, y) xRoot := Find(x) yRoot := Find(y) xRoot.parent := yRoot

여기서는 x가 포함된 집합을 y가 포함된 집합에 합치는 모습을 볼 수 있다.


의사코드를 봐도 매우 간단한 모습을 보인다.


set은 이진트리의 구조를 가지지만, 디스조인트-셋은 트리처럼 동작하는 parent 배열만 갖고 있어도 잘 동작한다.

이 점에서 속도의 장점을 얻는다.


위대하신 위키께서 이 과정을 그림으로 잘 설명해주셨다.



다섯 가지의 원소들로 집합질을 하는 과정을 잘 보여준다.


단점은 그래프구조를 모방한 구조에서 비롯된다. 대표자(루트노드)만을 내새운 방법 때문에, 다른 노드끼리의 그래프질은 불가능하다.


실제로 구현해보면 알겠지만, 대표번호만 맞을 뿐 속의 노드관계는 우리가 상상한 그래프와는 전혀 다른 모습을 가진다.

(비슷하게 하려면 더욱 개선된 디스조인트 연산들이 필요하다)


따라서 이 점 때문에 각 집합의 대표자만 연관된 문제들에 대해 이 자료구조를 적용할 수 있을 것이다.


나중에 최소신장트리를 구성하는데 이 자료구조를 활용하기도 한다. (아직 잘 모르니까 소개는 이정도로 ㅎㅎ)



2-1) 적용문제 1 : 집합의 표현

https://www.acmicpc.net/problem/1717


문제 제목부터 집합을 표현할 자료구조를 요구하고 있다.


{0}, {1}, {2}, {3}, ... {n} 의 부분집합들이 있다. 집합들을 합치기 전후로, 두 원소가 주어질 때, 두 원소가 같은 집합에 있는지 구해야 한다.


0~n까지의 부모번호를 저장하는 parent[] 배열을 준비한다. 각 원소는 자기자신의 번호로 초기화한다.


합집합 (0 a b)을 요구하면 union연산 (O(α(n)) = O(1)을,

같은 집합인지 알아보는 연산은(1 a b) 각각의 부모를 찾아( find(a), find(b) = (O(α(n)) = O(1))) 같은 부모(대표)인지 보면 된다.


코드 : http://js1jj2sk3.tistory.com/16?category=728064



2-2) 적용문제 2 : 여행 가자

https://www.acmicpc.net/problem/1976


그래프질이 필요한 문제다.


n개의 도시들이 주어지고, 각 도시를 잇는 도로가 있다.

여행계획이 주어질 때, 도로를 따라 계획대로 전부 방문할 수 있는지 없는지 판별해야 한다.


대충 생각해보자면, 여행계획대로 순서대로 실제로 그래프를 순회해보면서 방문가능한지를 계속 판별해야 할 것같다.


하지만 단순히 생각해보자면, 어쨌든 방문만 하면 되므로,

여행계획에 포함된 도시들이 같은 집합이기만 하면(신장트리라면) 방문 할 수 있다고 판별할 수 있다.


따라서 1번 도시와 2번도시가 연결되어 있다면 union(1, 2)으로 묶어주고(O(α(n)) = O(1)

여행계획의 도시들이 같은 집합인지 find()로 판별한다.(O(α(n))= O(1))


코드 : http://js1jj2sk3.tistory.com/17?category=728064


'알고리즘 > Disjoint-set 디스조인트-셋' 카테고리의 다른 글

백준) 10775 공항  (0) 2017.07.29
백준) 9938 방 청소  (0) 2017.07.29
백준) 4195 친구 네트워크  (0) 2017.07.28
백준) 1976 여행 가자  (0) 2017.07.28
백준) 1717 집합의 표현  (0) 2017.07.28

1202. 보석 도둑


문제

세계적인 도둑 상덕이는 보석점을 털기로 결심했다.

상덕이가 털 보석점에는 보석이 총 N개 있다. 각 보석은 무게 Mi와 가격 Vi를 가지고 있다. 상덕이는 가방을 K개 가지고 있고, 각 가방에 담을 수 있는 최대 무게는 Ci이다. 가방에는 최대 한 개의 보석만 넣을 수 있다.

상덕이가 훔칠 수 있는 보석의 최대 가격을 구하는 프로그램을 작성하시오.

입력

첫째 줄에 N과 K가 주어진다. (1 ≤ N, K ≤ 300,000)

다음 N개 줄에는 각 보석의 정보 Mi와 Vi가 주어진다. (1 ≤ Mi, Vi ≤ 1,000,000)

다음 K개 줄에는 가방에 담을 수 있는 최대 무게 Ci가 주어진다. (1 ≤ Ci ≤ 100,000,000)

모든 숫자는 양의 정수이다.

출력

첫째 줄에 상덕이가 훔칠 수 있는 보석 가격의 합의 최대값을 출력한다.

예제 입력 

3 2
1 65
5 23
2 99
10
2

예제 출력 

164



1. 접근


가방들에 최대한 비싼 보석들을 담아야 한다.


가방엔 보석을 하나만 넣을 수 있으므로 가장 비싼 보석을 넣어야한다.


가방마다의 부분문제로 이루어진 그리디 문제.



2. 풀이


탐욕적인 방법 : 가장 무게제한이 낮은 가방(A1)부터 최대한 비싼 보석을 넣는 방법을 쓰기로 한다.


정당성을 증명하기 위해, A1에 들어가는 보석들 중에 가장 비싼 보석(B)을 고르지 않는 방법이 실은 정답이라고 가정해보자.(귀류법)


무게 순으로 A1, A2, A3, ... 인 가방들의 특징은, A1이 담을 수 있는 보석은 당연히 A2도 담을 수 있다는 것이다.


따라서 직관적으로 가장 큰 가방만이 담을 수 있는 보석들이 있을 것이고, 최적해를 위해선 가장 비싼 보석을 골라야한다.


같은 의미로 A1도 A1이 담을 수 있는 가장 비싼 보석을 골라야한다. A1의 선택은 뒤의 선택에 영향을 미치지 않는다.


남은 부분문제도 첫 부분문제와 동치인 문제들이다.



3. 코드


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
#include <stdio.h>
#include <algorithm>
#include <queue>
using namespace std;
 
typedef pair<intint> p;
p jew[300000];
int bag[300000];
int n, k, m, v, c, i, j;
 
int main() {
    scanf("%d %d"&n, &k);
    for (i = 0; i < n; ++i) {
        scanf("%d %d"&jew[i].first, &jew[i].second);
    }
    for (i = 0; i < k; ++i)
        scanf("%d"&bag[i]);
 
    sort(jew, jew + n);
    sort(bag, bag + k);
 
    long long sum = 0;
    priority_queue<int> pq;
    for (i = 0, j = 0; i < k; ++i) {
        while (j < n && jew[j].first <= bag[i])
            pq.push(jew[j++].second);
        
        if (!pq.empty()) {
            sum += pq.top();
            pq.pop();
        }
    }
    printf("%lld", sum);
    return 0;
}
cs



4. 후기


컵라면 문제 (http://js1jj2sk3.tistory.com/12)와 많이 닮아 있는 문제다.


문제와 관련된 두 가지 조건(데드라인+컵라면 수 / 무게 제한 + 비싼 보석) 중에


원하는 정답조건(최대한 많은 컵라면 / 최대한 비싼 보석들의 합) 말고 다른 조건(데드라인 / 무게 제한) 순으로 정렬하고,


조건에 맞는 모든 경우를 수집한 다음(우선순위 큐에 저장한다음), 최적해를 고른다. 

'알고리즘 > Greedy 그리디' 카테고리의 다른 글

백준) 1911 흙길 보수하기  (0) 2017.07.25
백준) 1781 컵라면  (1) 2017.07.25
백준) 1931 회의실배정  (0) 2017.07.25
백준) 1946 신입 사원  (0) 2017.07.25
백준) 1744 수 묶기  (0) 2017.07.24

1911 흙길 보수하기


문제

어젯밤 겨울 캠프 장소에서 월드 본원까지 이어지는, 흙으로 된 비밀길 위에 폭우가 내려서 N (1 <= N <= 10,000) 개의 물웅덩이가 생겼다. 월드학원은 물웅덩이를 덮을 수 있는 길이 L (L은 양의 정수) 짜리 널빤지들을 충분히 가지고 있어서, 이들로 다리를 만들어 물웅덩이들을 모두 덮으려고 한다. 물웅덩이들의 위치와 크기에 대한 정보가 주어질 때, 모든 물웅덩이들을 덮기 위해 필요한 널빤지들의 최소 개수를 구하여라.

입력

첫째 줄에 N과 L이 들어온다.

둘째 줄부터 N+1번째 줄까지 총 N개의 줄에 각각의 웅덩이들의 정보가 주어진다. 웅덩이의 정보는 웅덩이의 시작 위치와 끝 위치로 이루어진다. 각 위치는 0이상 1,000,000,000이하의 정수이다.

출력

첫째 줄에 모든 물웅덩이들을 덮기 위해 필요한 널빤지들의 최소 개수를 출력한다.

예제 입력 

3 3
1 6
13 17
8 12

예제 출력 

5



1. 접근


당연히 처음부터 덮으면 된다!


그리디 알고리즘.



2. 풀이


매우 직관적으로 떠오르는 생각이 정답이다.


탐욕적 방법 : 맨 앞의 웅덩이의 처음부터 널빤지를 덮기로 하자.


다 덮고 나면 앞에서 덮은 널빤지가 바로 뒤의 웅덩이에 닿거나 닿지 않는 두 가지 케이스가 발생한다.


닿는다면 남은 웅덩이부분의 처음부터 다시 덮는 부분문제로 귀결되고, 닿지 않는다면 첫 웅덩이를 덮는 부분문제와 완전 동일하다.

따라서 정당성도 증명되었다.



3. 코드


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
36
37
38
39
40
41
42
#include <stdio.h>
#include <algorithm>
using namespace std;
 
typedef pair<intint> p;
bool cmp(const p& p1, const p& p2) {
    if (p1.first == p2.first)
        return p1.second > p2.second;
    else
        return p1.first < p2.first;
}
p pp[10001];
int n, l, i;
 
int main() {
    scanf("%d %d"&n, &l);
    for (i = 0; i < n; ++i)
        scanf("%d %d"&pp[i].first, &pp[i].second);
 
    sort(pp, pp + n, cmp);
    int last = 0, cnt = 0, tmp = 0;
    for (i = 0; i < n; ++i) {
        if (pp[i].second - 1 < last)
            continue;
        if (pp[i].first > last) {
            tmp = (pp[i].second - pp[i].first) / l;
            if ((pp[i].second - pp[i].first) % l != 0)
                tmp++;
            cnt += tmp;
            last = pp[i].first + l * tmp - 1;
        }
        else {
            tmp = (pp[i].second - last - 1/ l;
            if ((pp[i].second - last - 1) % l != 0)
                tmp++;
            cnt += tmp;
            last = last + l * tmp;
        }
    }
    printf("%d", cnt);
    return 0;
}
cs



4. 후기


처음엔 웅덩이들이 당연히 곂치지 않겠거니 하고 매우 간단하게 짰지만,


여러번 틀리는 것으로 보아 테스트케이스에 겹치는 웅덩이들도 있는 모양이다.

'알고리즘 > Greedy 그리디' 카테고리의 다른 글

백준) 1202 보석 도둑  (3) 2017.07.26
백준) 1781 컵라면  (1) 2017.07.25
백준) 1931 회의실배정  (0) 2017.07.25
백준) 1946 신입 사원  (0) 2017.07.25
백준) 1744 수 묶기  (0) 2017.07.24

+ Recent posts