최소 신장 트리
최소 신장 트리(Minimum Spanning Tree, MST)는 그래프의 모든 노드를 사이클 없이 연결하면서, 사용한 간선의 가중치 합을 최소로 만드는 부분 그래프입니다. algo-05(다익스트라)와 같은 그래프를 쓰지만, 다익스트라는 "한 지점에서 각 지점까지의 최단 거리"를 구하고 MST는 "전체를 잇는 가장 싼 연결망"을 구한다는 점이 다릅니다.
핵심 개념
신장 트리(Spanning Tree)
그래프의 모든 노드를 포함하면서 사이클이 없는 부분 그래프입니다. 노드가 n개인 연결 그래프의 신장 트리는 간선이 정확히 n-1개입니다.
핵심 개념
크루스칼(Kruskal) — 간선 중심
모든 간선을 가중치가 작은 순서로 정렬한 뒤, 사이클을 만들지 않는 간선만 골라 추가합니다. 이미 연결된 두 노드를 다시 연결하면 사이클이 생기므로 건너뜁니다.
핵심 개념
프림(Prim) — 노드 중심
시작 노드에서 출발해, 지금까지 만든 트리와 트리 밖 노드를 잇는 가장 싼 간선을 매번 골라 트리를 한 노드씩 키워나갑니다. algo-05 다익스트라와 거의 같은 골격입니다.
핵심 개념
사이클 검사 (유니온-파인드)
크루스칼에서 "두 노드가 이미 연결되어 있는지"를 빠르게 확인하기 위해 유니온-파인드(Union-Find) 자료구조를 사용합니다. 같은 그룹이면 연결 시 사이클이 생깁니다.
핵심 개념
둘 다 그리디다
크루스칼은 "가장 싼 간선"을, 프림은 "트리에 연결하는 가장 싼 간선"을 매번 선택하고 절대 번복하지 않습니다. algo-07(그리디)과 달리, MST 문제에서는 이 그리디 선택이 최적해를 보장한다고 증명되어 있습니다.
핵심 개념
같은 그래프, 같은 답
이 시각화의 두 알고리즘은 서로 다른 순서로 진행하지만, 최종 MST의 총 가중치는 정확히 같습니다(13). 접근 방식이 달라도 "최소"라는 답 자체는 하나로 수렴합니다.
최소 신장 트리의 역사
최소 신장 트리(MST) 알고리즘은 20세기 전반부터 여러 사람이 따로 발견한 것으로 알려져 있습니다. 보루프카(Borůvka)가 1926년에, 크루스칼(Kruskal)이 1956년에, 프림(Prim)이 1957년에 발표했습니다. 프림의 알고리즘은 이보다 앞서 야르니크(Jarník)가 1930년에 발표한 것과 같아서 프림–야르니크 알고리즘이라고도 부릅니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
크루스칼과 프림의 차이는 무엇인가요?
크루스칼은 모든 간선을 가중치순으로 보며 사이클이 안 생기는 것만 고르고, 프림은 한 노드에서 시작해 현재 트리와 바깥을 잇는 가장 싼 간선으로 트리를 키웁니다.
면접 질문
유니온-파인드는 왜 필요한가요?
간선을 추가했을 때 사이클이 생기는지는 두 노드가 이미 같은 집합에 있는지로 판단할 수 있고, 유니온-파인드는 이를 거의 상수 시간에 확인하고 합칩니다.
면접 질문
MST가 유일하지 않을 수 있는 이유는 무엇인가요?
가중치가 같은 간선이 여러 개 있으면 어느 것을 먼저 고르느냐에 따라 구성이 달라지지만, 총 가중치는 같습니다.
면접 질문
프림과 다익스트라는 어떻게 다른가요?
둘 다 우선순위 큐로 가장 가까운 노드를 고르는 같은 골격이지만, 다익스트라는 시작점에서의 누적 거리를, 프림은 트리에 연결하는 간선 하나의 가중치를 기준으로 합니다.
실제 시스템에서는
minimum_spanning_tree는 알고리즘으로 kruskal(기본), prim, boruvka를 고를 수 있습니다.
함께 보면 좋은 토픽
참고 자료
Java — 크루스칼 (유니온-파인드로 사이클 검사)
/* ── 가중치 오름차순 정렬 후, 사이클을 안 만들면 추가 ──────── */ public int kruskal(int n, int[][] edges) { // edges[i] = {u, v, weight} Arrays.sort(edges, (a, b) -> a[2] - b[2]); // 가중치 기준 정렬 int[] parent = new int[n]; for (int i = 0; i < n; i++) parent[i] = i; int totalWeight = 0, edgeCount = 0; for (int[] e : edges) { int ru = find(parent, e[0]), rv = find(parent, e[1]); if (ru != rv) { // 서로 다른 그룹 → 연결해도 사이클 없음 parent[ru] = rv; totalWeight += e[2]; edgeCount++; if (edgeCount == n - 1) break; // 간선을 n-1개 모으면 완성 } } return totalWeight; } private int find(int[] parent, int x) { while (parent[x] != x) x = parent[x]; // x가 속한 그룹의 대표 노드 찾기 return x; }
find(parent, u) != find(parent, v) 한 줄이 "사이클이 생기는지" 판단의 전부입니다 — 두 노드가 이미 같은 그룹(같은 대표 노드)이면 연결 시 반드시 사이클이 생깁니다.
정렬에 O(E log E), 유니온-파인드 연산은 사실상 O(1)에 가까워 전체는 O(E log E)입니다.
algo-07(그리디)의 활동 선택처럼 "정렬 후 한 번 훑기"와 같은 패턴입니다.
Java — 프림 (algo-05 다익스트라와 거의 동일한 골격)
/* ── "누적 거리" 대신 "트리에 연결하는 간선 하나의 가중치"만 비교 ── */ public int prim(int n, List<List<int[]>> adj, int start) { boolean[] inTree = new boolean[n]; int[] minEdge = new int[n]; Arrays.fill(minEdge, Integer.MAX_VALUE); minEdge[start] = 0; int totalWeight = 0; for (int count = 0; count < n; count++) { int u = -1; for (int i = 0; i < n; i++) { // 트리 밖에서 연결 비용이 가장 싼 노드 탐색 if (!inTree[i] && (u == -1 || minEdge[i] < minEdge[u])) u = i; } inTree[u] = true; totalWeight += minEdge[u]; for (int[] edge : adj.get(u)) { // u의 인접 간선으로 연결 비용 갱신 int v = edge[0], w = edge[1]; if (!inTree[v] && w < minEdge[v]) minEdge[v] = w; } } return totalWeight; }
다익스트라 코드와 나란히 놓고 비교해보면 구조가 거의 동일합니다 — 차이는 minEdge[v] = w 딱 한 줄입니다. 다익스트라는 dist[u] + w(누적 거리)와 비교하지만, 프림은 w(간선 하나의 가중치)만 비교합니다.
"지금까지 온 총 거리"가 중요한가, "트리에 새로 연결하는 이 간선 하나"가 중요한가 — 이 관점 차이가 최단 경로 문제와 MST 문제를 가릅니다.
Java — 유니온-파인드 경로 압축 (최적화)
/* ── find할 때마다 트리를 납작하게 눌러 다음 find를 더 빠르게 ── */ private int findCompressed(int[] parent, int x) { if (parent[x] != x) { parent[x] = findCompressed(parent, parent[x]); // 재귀로 대표 노드를 찾은 뒤 } return parent[x]; // x를 대표 노드에 직접 연결(경로 압축) }
기본 find는 그룹이 길게 이어져 있으면 대표 노드까지 거슬러 올라가는 데 시간이 걸립니다.
경로 압축은 한 번 찾은 뒤 중간 노드들을 전부 대표 노드에 직접 연결해, 다음번 find는 거의 O(1)에 끝나게 만듭니다.
이 시각화의 크루스칼은 간단한 버전(경로 압축 없음)으로 동작하지만, 그래프가 커지면 이 최적화가 성능에 큰 차이를 만듭니다.
Q1
최소 신장 트리(MST)의 정의는?
Q2
크루스칼(Kruskal) 알고리즘의 방식은?
Q3
프림(Prim) 알고리즘과 크루스칼의 차이는?