본 시리즈는 프로그래밍 알고리즘의 개념을 정리하고 실습을 진행해보는 시리즈입니다.
Minimum Spanning Tree(MST)는 가중치가 있는 무향 그래프에서 모든 노드를 연결하는 트리 중 가중치의 합이 가장 작은 트리를 의미합니다.
MST는 네트워크 설계, 경로 최적화 등 여러 분야에서 사용되며, 대표적인 예로는 전기나 수도와 같은 인프라 네트워크 설계, 도로 시스템 최적화, 그리고 클러스터링 문제 등이 있습니다.
MST의 주요 특징은 다음과 같습니다:
노드 수 - 1
개입니다.이러한 MST를 찾기 위해 주로 사용되는 알고리즘은 다음과 같습니다:
최소 신장 트리(MST)를 찾기 위한 대표적인 알고리즘은 크루스칼(Kruskal) 알고리즘과 프림(Prim) 알고리즘입니다. 이 두 알고리즘은 모두 MST를 찾기 위해 사용되지만, 작동 방식과 적합한 그래프 구조에서 차이가 있습니다.
크루스칼(Kruskal) 알고리즘
Union-Find
자료구조를 이용해 사이클을 감지하며, 네트워크 연결 문제에 많이 사용됩니다.프림(Prim) 알고리즘
알고리즘 | 주요 특징 | 시간 복잡도 | 적합한 그래프 구조 |
---|---|---|---|
크루스칼 (Kruskal) | 간선 선택 방식, 사이클 감지 | 희소 그래프 (sparse graph) | |
프림 (Prim) | 노드 중심으로 확장, 우선순위 큐 활용 | 밀집 그래프 (dense graph) |
Kruskal 알고리즘은 그리디(Greedy) 알고리즘의 한 예로, 최소 가중치 간선부터 하나씩 선택하면서 MST를 형성합니다.
Kruskal 알고리즘의 동작 원리는 다음과 같습니다:
V
는 노드의 수입니다.)이 과정을 통해 그래프의 모든 노드를 연결하면서 최소 비용의 신장 트리를 완성할 수 있습니다.
Union-Find 자료구조란?
Union-Find 자료구조는 서로소 집합(disjoint set)을 효율적으로 관리하는 자료구조로, Kruskal 알고리즘에서 사이클을 검출하기 위해 사용됩니다.
이를 통해 간선을 추가할 때 사이클이 생기는지 빠르게 확인할 수 있습니다.
- Find: 특정 노드가 속한 집합의 대표(root) 노드를 찾습니다.
- Kruskal 알고리즘에서 각 노드가 연결된 집합을 추적하여, 두 노드가 같은 집합에 속하는지 여부를 확인하는 데 사용됩니다.
- 경로 압축(Path Compression)을 사용해 Find 연산을 최적화할 수 있습니다.
- 이는 노드를 찾을 때 해당 경로를 최적화하여 다음 검색이 더 빠르게 이뤄지도록 하는 기법입니다.
- Union: 두 집합을 하나의 집합으로 합칩니다.
- 두 노드가 서로 다른 집합에 속할 경우, 두 집합을 하나로 합쳐 MST에 추가할 수 있도록 합니다.
- Union 연산에서는 일반적으로 랭크(rank) 기반 또는 크기(size) 기반 최적화 기법을 사용해 트리의 높이를 최소화합니다.
- 이와 같은 Union-Find 자료구조는 Kruskal 알고리즘에서 사이클 검출을 빠르게 수행해 주며, 알고리즘의 시간 효율성을 높이는 데 중요한 역할을 합니다.
Java와 Python으로 구현된 Kruskal 알고리즘의 예제는 다음과 같습니다. 이 코드는 Union-Find 자료구조를 활용하여 효율적으로 사이클을 검출하고, MST를 형성합니다.
Java 코드 구현
import java.util.*;
public class Kruskal {
// 간선을 나타내는 Edge 클래스 - Comparable 인터페이스를 구현하여 간선 가중치 기준으로 정렬 가능
static class Edge implements Comparable<Edge> {
int src, dest, weight;
public Edge(int src, int dest, int weight) {
this.src = src;
this.dest = dest;
this.weight = weight;
}
// 가중치 기준 오름차순 정렬을 위한 compareTo 메서드
public int compareTo(Edge other) {
return this.weight - other.weight;
}
}
// Union-Find에서 각 노드의 부모를 저장하는 배열
static int[] parent;
// Find 연산 - 경로 압축 최적화를 적용하여 트리의 높이를 줄임
static int find(int i) {
if (parent[i] != i) {
parent[i] = find(parent[i]);
}
return parent[i];
}
// Union 연산 - 두 집합을 병합
static void union(int x, int y) {
int xRoot = find(x);
int yRoot = find(y);
parent[xRoot] = yRoot;
}
// Kruskal 알고리즘 - MST를 생성하고 각 간선 및 최종 가중치 출력
static void kruskalMST(int V, List<Edge> edges) {
// 간선 정렬 - 가중치가 낮은 순서대로 정렬
edges.sort(null);
parent = new int[V]; // Union-Find 초기화
for (int i = 0; i < V; i++) parent[i] = i;
int edgeCount = 0, mstWeight = 0;
for (Edge edge : edges) {
// MST 완성 조건 - V-1개의 간선이 선택되면 종료
if (edgeCount == V - 1) break;
int x = find(edge.src);
int y = find(edge.dest);
if (x != y) { // 사이클을 형성하지 않으면 간선 선택
union(x, y);
edgeCount++;
mstWeight += edge.weight;
System.out.println("Selected Edge: " +
edge.src + " - " +
edge.dest + " : " +
edge.weight);
}
}
System.out.println("Total weight of MST: " + mstWeight);
}
public static void main(String[] args) {
int V = 4; // 노드 수
List<Edge> edges = new ArrayList<>();
// 간선 추가 (src, dest, weight)
edges.add(new Edge(0, 1, 10));
edges.add(new Edge(0, 2, 6));
edges.add(new Edge(0, 3, 5));
edges.add(new Edge(1, 3, 15));
edges.add(new Edge(2, 3, 4));
// Kruskal 알고리즘 실행
kruskalMST(V, edges);
}
}
출력 결과 (Java, Python 동일)
Selected Edge: 2 - 3 : 4
Selected Edge: 0 - 3 : 5
Selected Edge: 0 - 1 : 10
Total weight of MST: 19
Java 코드 구현 설명
위 Java 코드는 Kruskal 알고리즘을 통해 최소 신장 트리(MST)를 찾는 방법을 보여줍니다. 주요 기능은 다음과 같습니다:
edges.sort(null);
를 통해 간선 리스트를 가중치 기준으로 오름차순 정렬합니다.find
와 union
메서드를 통해 각 노드가 속한 집합을 추적하고, 간선 선택 시 사이클 여부를 검사합니다.이 알고리즘은 의 시간 복잡도를 가지며, 간선이 정렬된 후 간선이 개 선택될 때까지 트리를 확장해 나갑니다.
Python 코드 구현
class Edge:
def __init__(self, src, dest, weight):
self.src = src
self.dest = dest
self.weight = weight
# Find 연산 - 경로 압축을 통해 트리 높이 최적화
def find(parent, i):
if parent[i] != i:
parent[i] = find(parent, parent[i])
return parent[i]
# Union 연산 - 두 집합을 하나로 병합
def union(parent, x, y):
x_root = find(parent, x)
y_root = find(parent, y)
parent[x_root] = y_root
# Kruskal 알고리즘 - MST 생성 및 간선 출력
def kruskal_mst(vertices, edges):
# 1. 간선 정렬 - 가중치 기준 오름차순
edges.sort(key=lambda e: e.weight)
parent = [i for i in range(vertices)] # Union-Find 초기화
mst_weight = 0
for edge in edges:
x = find(parent, edge.src)
y = find(parent, edge.dest)
if x != y: # 사이클이 없을 경우 간선 선택
union(parent, x, y)
mst_weight += edge.weight
print(f"Selected Edge: {edge.src} - {edge.dest} : {edge.weight}")
print(f"Total weight of MST: {mst_weight}")
# 테스트 실행
v = 4
edges_list = [
Edge(0, 1, 10),
Edge(0, 2, 6),
Edge(0, 3, 5),
Edge(1, 3, 15),
Edge(2, 3, 4),
]
kruskal_mst(v, edges_list)
출력 결과 (Java, Python 동일)
Selected Edge: 2 - 3 : 4
Selected Edge: 0 - 3 : 5
Selected Edge: 0 - 1 : 10
Total weight of MST: 19
Python 코드 구현 설명
위 Python 코드는 Java와 동일한 Kruskal 알고리즘을 사용해 MST를 구성합니다. 주요 구현 방식은 다음과 같습니다:
edges.sort(key=lambda e: e.weight)
로 간선의 가중치를 기준으로 오름차순 정렬합니다.find
와 union
함수를 사용해 각 노드의 집합을 추적하고, 사이클 검출을 수행합니다.이 Python 구현도 의 시간 복잡도를 가지며, 가중치가 낮은 간선부터 선택하여 MST를 형성해 나가는 과정은 Java와 동일합니다.
Prim 알고리즘은 그리디(Greedy) 알고리즘의 또 다른 예로, 특정 시작 노드에서 출발하여 인접한 간선 중 가중치가 가장 낮은 간선을 선택하며 트리를 확장해 나가는 방식으로 최소 신장 트리(MST)를 형성합니다.
Prim 알고리즘의 동작 원리는 다음과 같습니다:
이 과정을 통해 그래프의 모든 노드를 연결하면서 최소 비용의 신장 트리를 완성할 수 있습니다.
Java와 Python으로 구현된 Prim 알고리즘의 예제는 다음과 같습니다.
Java 코드 구현
import java.util.*;
public class Prim {
static class Edge implements Comparable<Edge> {
int dest, weight;
public Edge(int dest, int weight) {
this.dest = dest;
this.weight = weight;
}
public int compareTo(Edge other) {
return this.weight - other.weight;
}
}
static void primMST(List<List<Edge>> graph, int V) {
boolean[] inMST = new boolean[V];
Queue<Edge> pq = new PriorityQueue<>();
pq.add(new Edge(0,0)); // 시작 노드는 가중치 0으로 추가
int mstWeight = 0;
while (!pq.isEmpty()) {
Edge edge = pq.poll();
int node = edge.dest;
// 노드가 MST에 포함되었는지 확인
if (inMST[node]) continue;
// 노드를 MST에 추가하고 가중치 누적
inMST[node] = true;
mstWeight += edge.weight;
System.out.println("Selected Node: " + node +
" with edge weight: " + edge.weight);
// 인접 노드들을 탐색
for (Edge adjacent : graph.get(node)) {
if (!inMST[adjacent.dest]) {
pq.add(adjacent);
}
}
}
System.out.println("Total weight of MST: " + mstWeight);
}
public static void main(String[] args) {
int V = 4; // 노드 수
List<List<Edge>> graph = new ArrayList<>();
for (int i = 0; i < V; i++) graph.add(new ArrayList<>());
// 양방향 간선 추가 (src, dest, weight)
graph.get(0).add(new Edge(1, 10));
graph.get(1).add(new Edge(0, 10));
graph.get(0).add(new Edge(2, 6));
graph.get(2).add(new Edge(0, 6));
graph.get(0).add(new Edge(3, 5));
graph.get(3).add(new Edge(0, 5));
graph.get(1).add(new Edge(3, 15));
graph.get(3).add(new Edge(1, 15));
graph.get(2).add(new Edge(3, 4));
graph.get(3).add(new Edge(2, 4));
// Prim 알고리즘 실행
primMST(graph, V);
}
}
출력 결과 (Java, Python 동일)
Selected Node: 0 with edge weight: 0
Selected Node: 3 with edge weight: 5
Selected Node: 2 with edge weight: 4
Selected Node: 1 with edge weight: 10
Total weight of MST: 19
Java 코드 구현 설명
위 Java 코드는 Prim 알고리즘을 통해 MST를 생성하는 방법을 보여줍니다. 주요 기능은 다음과 같습니다:
PriorityQueue
를 통해 가중치가 낮은 간선을 우선으로 선택합니다.이 알고리즘은 의 시간 복잡도를 가지며, Prim 알고리즘의 특징인 노드 중심의 확장 방식을 잘 보여줍니다.
Python 코드 구현
import heapq
def prim_mst(graph, vertices):
in_mst = [False] * vertices
pq = [(0, 0)] # (가중치, 노드)
mst_weight = 0
while pq:
weight, node = heapq.heappop(pq)
# 노드가 이미 MST에 포함되었는지 확인
if in_mst[node]:
continue
# 노드를 MST에 추가하고 가중치 누적
in_mst[node] = True
mst_weight += weight
print(f"Selected Node: {node} with edge weight: {weight}")
# 인접 노드들을 탐색
for adj_weight, adj_node in graph[node]:
if not in_mst[adj_node]:
heapq.heappush(pq, (adj_weight, adj_node))
print(f"Total weight of MST: {mst_weight}")
# 테스트 실행
v = 4
graph = {
0: [(10, 1), (6, 2), (5, 3)],
1: [(10, 0), (15, 3)],
2: [(6, 0), (4, 3)],
3: [(5, 0), (15, 1), (4, 2)],
}
prim_mst(graph, v)
출력 결과 (Java, Python 동일)
Selected Node: 0 with edge weight: 0
Selected Node: 3 with edge weight: 5
Selected Node: 2 with edge weight: 4
Selected Node: 1 with edge weight: 10
Total weight of MST: 19
Python 코드 구현 설명
위 Python 코드는 Prim 알고리즘을 사용해 MST를 구성합니다. 주요 기능은 다음과 같습니다:
heapq
모듈을 통해 가중치가 낮은 간선을 우선으로 선택합니다.Python 구현에서도 의 시간 복잡도를 가지며, Java와 동일한 노드 중심의 트리 확장 방식으로 MST를 형성합니다.
Kruskal과 Prim 알고리즘은 모두 MST를 찾기 위한 대표적인 방법이지만, 각각의 특성과 장단점이 있어 상황에 맞는 알고리즘을 선택하는 것이 중요합니다.
아래는 두 알고리즘의 주요 차이점과 선택 기준을 정리한 표입니다.
알고리즘 | 시간 복잡도 | 주요 특징 | 적합한 그래프 구조 |
---|---|---|---|
Kruskal | 간선 중심, Union-Find로 사이클 감지 | 간선이 적은 경우 (희소 그래프) | |
Prim | 노드 중심, 우선순위 큐로 인접 간선 선택 | 노드 수가 많고 간선이 많은 경우 (밀집 그래프) |
따라서 그래프의 간선 밀도와 노드 수에 따라 두 알고리즘 중 적합한 방식을 선택하는 것이 좋습니다.
Minimum Spanning Tree(MST)는 그래프에서 모든 노드를 연결하면서도 가중치의 합이 최소가 되도록 트리를 구성하는 핵심 개념입니다. MST는 네트워크 최적화, 경로 탐색, 클러스터링 문제 등 다양한 분야에서 널리 활용됩니다.
본 포스팅에서는 Kruskal 알고리즘과 Prim 알고리즘을 통해 MST를 구성하는 방법을 살펴보았고, 각 알고리즘의 특징과 코드 구현을 통해 작동 원리를 살펴보았습니다.