개념
동아리를 떠올려 봐요. 두 사람이 같은 동아리인지 알고 싶으면, 각자에게 '너희 회장이 누구야?'라고 물어 회장이 같은지만 보면 돼요. 유니온 파인드가 딱 이거예요 — 무리마다 대표(루트) 한 명을 두고, 같은 무리인지는 대표가 같은지로 판정해요. 두 동아리가 합쳐지면 한쪽 회장이 다른 쪽 회장 밑으로 들어가고, 그 뒤론 모두가 하나의 대표를 가리켜요.15주차에서 연결 요소를 DFS로 셌어요. 그런데 간선이 하나씩 추가되는 상황이라면 매번 DFS를 다시 돌려야 해요. 유니온 파인드는 합치기와 같은 무리인지 확인하기를 각각 거의
간선을 V-1개 고르는 순간 멈춰도 돼요. 그리디가 여기서 통하는 이유는 '가장 싼 간선은 항상 어떤 MST에 포함된다'는 성질 때문이에요.
코딩 테스트에서는 크루스칼 하나로 대부분 통과해요. 다만 간선 목록이 아니라 인접 행렬로 주어지는 밀집 그래프(모든 도시 쌍의 거리가 전부 주어지는 문제)에서는 간선이
O(1)에 처리해요.아이디어는 각 무리마다 대표 하나를 정해두는 거예요. find(x)는 x가 속한 무리의 대표를 찾고, union(a, b)는 두 무리의 대표를 하나로 합쳐요. 같은 무리인지는 대표가 같은지로 판정해요.두 가지 최적화
순진하게 구현하면 트리가 한 줄로 길어져find가 O(n)이 돼요. 두 가지를 함께 쓰면 사실상 상수 시간이 돼요.- 경로 압축 —
find하는 김에 지나온 노드를 전부 대표에 직접 붙여요. 다음부터 한 번에 도달해요 - union by rank/size — 합칠 때 작은 무리를 큰 무리 밑에 붙여요. 트리가 깊어지지 않아요
사이클 판정
간선(a, b)를 추가하려는데 a와 b가 이미 같은 무리라면, 그 간선은 사이클을 만들어요. 이 판정이 다음에 나올 크루스칼의 핵심 부품이에요.최소 신장 트리 (MST)
모든 정점을 연결하되 간선 비용의 합이 최소가 되게 고른 것을 최소 신장 트리라 해요. 정점이 V개면 간선은 정확히 V-1개이고 사이클이 없어요. '모든 도시를 잇는 최소 비용 도로망' 같은 문제가 이거예요.섬 여러 개를 다리로 잇는다고 생각해 봐요. 가장 싼 다리부터 놓되, 이미 이어진 두 섬을 또 잇는 다리는 필요 없으니 건너뛰어요(그게 사이클이에요). 이렇게만 하면 최소 비용으로 전부 이어져요. 이게 크루스칼이에요.크루스칼은 그리디예요(9주차). 간선을 비용 오름차순으로 정렬해놓고 싼 것부터 집되, 사이클을 만드는 간선만 건너뛰어요. 유니온 파인드가 바로 그 사이클 판정을 담당해요.| 단계 | 도구 | 복잡도 |
|---|---|---|
| 간선 정렬 | 정렬 (7주차) | O(E log E) |
| 사이클 판정 | 유니온 파인드 | 거의 O(1) |
| 전체 | 크루스칼 | O(E log E) |
직접 따라가 보기: 크루스칼
정점 4개, 간선 5개짜리 그래프에서 MST를 만들어 볼게요. 먼저 간선을 비용 오름차순으로 정렬해두고, 싼 것부터 하나씩 봐요. 두 끝점의 대표가 다르면 잇고(union), 이미 같으면 사이클이니 버려요. parent 배열이 어떻게 합쳐지는지 함께 볼게요. 처음엔 각자 자기가 대표예요.간선을 싼 순서로 보며 union, 사이클이면 버리기간선 (정점a, 정점b, 비용)을 비용순으로:(0,1,1) (1,2,2) (0,2,3) (2,3,4) (1,3,5)parent 시작 [ 0 1 2 3 ] 총비용 0(0,1,1) 대표 0!=1 -> 잇기, 1의 대표를 0으로parent [ 0 0 2 3 ] 총비용 1(1,2,2) 대표 0!=2 -> 잇기, 2의 대표를 0으로parent [ 0 0 0 3 ] 총비용 3(0,2,3) 대표 0==0 -> 사이클! 버림parent [ 0 0 0 3 ] 총비용 3(2,3,4) 대표 0!=3 -> 잇기, 3의 대표를 0으로parent [ 0 0 0 0 ] 총비용 7간선 3개(=V-1) 골랐으니 멈춤 -> MST 비용 = 7
(0,2,3)을 버린 게 핵심이에요. 그 시점에 0과 2는 이미 0-1-2로 이어져 있어서, 3짜리 간선을 더하면 삼각형(사이클)이 생겨요. 대표가 같은지 물어보는 것만으로 그걸 알아채고 건너뛰었어요. 이 판정을 유니온 파인드가 거의 상수 시간에 해주기 때문에 크루스칼이 빠른 거예요.프림
같은 MST를 다른 방향에서 만들어요. 크루스칼이 간선을 싼 것부터 집었다면, 프림은 정점을 하나씩 키워요. 시작 정점 하나를 무리에 넣고, 무리 밖으로 나가는 간선 중 가장 싼 것을 골라 그 끝 정점을 무리에 넣기를 V-1번 반복해요.구조가 다익스트라와 거의 같아요 — 힙에서 가장 싼 것을 꺼내고, 이미 무리에 든 정점이면 건너뛰어요. 다른 점은 하나뿐이에요. 다익스트라는 힙에 시작점부터의 누적 거리를 넣고, 프림은 간선 하나의 비용만 넣어요.| 크루스칼 | 프림 | |
|---|---|---|
| 키우는 대상 | 간선 집합 | 정점 무리 하나 |
| 필요한 도구 | 정렬 + 유니온 파인드 | 힙 |
| 복잡도 | O(E log E) | O(E log V) |
| 유리한 그래프 | 간선이 적을 때 (희소) | 간선이 많을 때 (밀집) |
V²개라 정렬 비용이 부담스럽고, 그때 프림이 자연스러워요.패턴 코드
유니온 파인드 (경로 압축 + union by size)
javascriptCopy codeconst parent = Array.from({ length: n }, (_, i) => i);const size = new Array(n).fill(1);function find(x) {while (parent[x] !== x) {parent[x] = parent[parent[x]]; // 경로 압축x = parent[x];}return x;}function union(a, b) {let ra = find(a);let rb = find(b);if (ra === rb) return false; // 이미 같은 무리 = 사이클if (size[ra] < size[rb]) [ra, rb] = [rb, ra];parent[rb] = ra;size[ra] += size[rb];return true;}
크루스칼 MSTunion이 false를 돌려주면 그 간선은 사이클을 만든다는 뜻이라 건너뛰어요.
javascriptCopy codeedges.sort((a, b) => a[2] - b[2]);let total = 0;let used = 0;for (const [a, b, cost] of edges) {if (union(a, b)) {total += cost;if (++used === n - 1) break;}}
프림 MST다익스트라와 뼈대가 같아요. 힙에 넣는 값이 누적 거리가 아니라 간선 비용이에요.
javascriptCopy codeconst inTree = new Array(n).fill(false);const heap = new MinHeap((a, b) => a[0] - b[0]);heap.push([0, 0]); // [비용, 정점]let total = 0;let used = 0;while (heap.size > 0 && used < n) {const [cost, v] = heap.pop();if (inTree[v]) continue; // 이미 무리에 든 정점inTree[v] = true;total += cost;used++;for (const [next, w] of graph[v]) {if (!inTree[next]) heap.push([w, next]);}}
이번 주 문제
이번 주 진행0 / 4
이번 주 주제로 골라둔 문제예요. 채점은 프로그래머스에서 하고, 풀고 나면 여기에 체크해 두세요. 난이도 순으로 보여줘요.
- 섬 연결하기Lv. 3풀기
셀프 체크
다음 주로 넘어가기 전에 소리 내어 답해 보세요. 막히면 그 부분이 아직 덜 익은 개념이에요.- 경로 압축을 빼면 어떤 입력에서 O(n²)가 되나요?
- 두 정점이 같은 무리인지 볼 때 왜
find(a) === find(b)로 비교해야 하나요? - 유니온 파인드로 사이클을 판정하는 원리는 무엇인가요?
- 크루스칼이 그리디로 정당한 이유를 한 문장으로 말할 수 있나요?
- 프림의 힙에 넣는 값이 다익스트라와 어떻게 다른가요?