DFS/BFS와 최단경로 알고리즘(다익스트라, 플로이드워셜) 모두 그래프 알고리즘의 한 유형으로 볼 수 있다. 이번에는 이것들 외의 그래프 알고리즘인 서로소 집합 알고리즘, 크루스칼 알고리즘, 위상정렬 알고리즘을 살펴본다.

크루스칼은 그리디 알고리즘으로 분류되고, 위상정렬은 큐나 스택을 활용해서 구현한다.


1. 신장 트리

신장 트리는 모든 노드를 포함하면서 사이클이 존재하지 않는 부분 그래프이다. 여기서 모든 노드를 포함하면서 사이클이 존재하지 않는다는 조건은 트리의 조건이다.

image

아래 그림에서 왼쪽은 그래프가 노드1을 포함하고 있지 않기 때문에 신장 트리에 해당하지 않는다. 오른쪽은 사이클이 존재하므로 신장 트리가 아니다.

image


2. 크루스칼 알고리즘 (Kruskal)

최소 신장 트리 알고리즘은 최소 비용으로 만들 수 있는 신장 트리를 찾는 알고리즘이다. 크루스칼 알고리즘은 최소 신장 트리 알고리즘 중 하나로 그리디 알고리즘으로 분류된다. 모든 간선에 대해 정렬을 수행한 후에 거리가 짧은 간선부터 집합에 포함시키면 된다. 사이클을 발생시키는 간선은 집합에 포함시키지 않는다.

① 간선은 비용에 따라 오름차순으로 정렬한다.

② 간선을 하나씩 확인하며 현재의 간선이 사이클을 발생시키지 않는 경우 최소 신장 트리에 포함한다.

③ 모든 간선에 대해 2번을 반복한다.

최소 신장 트리의 간선의 개수 = 노드의 개수 - 1 이라는 특징이 있다. 다음과 같은 예를 보며 이해해보자.

image

먼저 간선 비용에 따라 오름차순 정렬을 한다.

image

가장 짧은 간선을 (3,4)를 선택하고 집합에 포함한다. 즉, 3번 노드와 4번 노드에 대해 union함수를 수행한다.

image

그 다음 비용이 작은 (4,7)을 선택하고, 4번과 7번 노드는 같은 집합에 속해있지 않기 때문에 union함수를 수행한다.

image

그 다음 비용이 작은 (4,6)을 선택하고, 4번과 7번 노드는 같은 집합에 속해있지 않기 때문에 union함수를 수행한다.

image

그 다음 비용이 작은 (6,7)을 선택하고, 6번과 7번 노드의 루트가 같은 집합에 속해있기 때문에 건너뛴다.

image

그 다음 비용이 작은 (1,2)을 선택하고, 1번과 2번 노드는 같은 집합에 속해있지 않기 때문에 union함수를 수행한다.

image

그 다음 비용이 작은 (2,6)을 선택하고, 2번과 6번 노드는 같은 집합에 속해있지 않기 때문에 union함수를 수행한다.

image

그 다음 비용이 작은 (2,3)을 선택하고, 2번과 3번 노드의 루트가 같은 집합에 속해있기 때문에 건너뛴다.

image

그 다음 비용이 작은 (5,6)을 선택하고, 5번과 6번 노드는 같은 집합에 속해있지 않기 때문에 union함수를 수행한다.

image

그 다음 비용이 작은 (1,5)을 선택하고, 1번과 5번 노드의 루트가 같은 집합에 속해있기 때문에 건너뛴다.

image

결과적으로 다음 그림과 같이 최소 신장 트리가 만들어진다.

image

여기서 간선의 비용만 모두 더하면 그 값이 최종 비용이 된다.


3. Kruskal with Python

크루스칼 알고리즘은 서로소 집합과 비슷하다.

# 특정 원소가 속한 집합 찾기 (루트 찾기) 
def find_parent(parent, x):
    if parent[x] != x:  # x의 루트가 x가 아니면 루트를 찾을 때까지 재귀적 호줄
        parent[x] = find_parent(parent, parent[x])
    return parent[x]

# 두 원소가 속한 집합 합치기
def union_parent(parent, a, b):
    a = find_parent(parent, a)
    b = find_parent(parent, b)
    if a < b:
        parent[b] = a
    else:
        parent[a] = b
        
# union 연산 입력 받기 
v, e = map(int, input().split())  # 노드의 개수 v, 간선의 개수 e
parent = [0] * (v+1)  # 부모 테이블 초기화 
for i in range(1, v+1):  # 부모 테이블에서 부모를 자기 자신으로 초기화
    parent[i] = i  

edges = []  # 간선을 담을 리스트
result = 0  # 부모 테이블 초기화

# union 연산 수행
for i in range(e):
    a, b, cost = map(int, input().split())
    edges.append((cost, a, b))  # 비용순으로 정렬하기 위해 튜플의 첫 원소를 비용으로 설정 

edges.sort()  # 비용순으로 정렬
for edge in edges:
    cost, a, b = edge
    if find_parent(parent,a) != find_parent(parent,b):  # 사이클 발생하지 않는 경우만 집합에 포함
        union_parent(parent, a, b)
        result += cost

print(result)

'''
7 9
1 2 29
1 5 75
2 3 35
2 6 34
3 4 7
4 6 23
4 7 13
5 6 53
6 7 25
159
'''

4. 크루스칼 시간 복잡도: O(ElogE)

크루스칼은 간선의 개수가 E개 일때 O(ElogE)의 시간 복잡도를 가진다. 그 이유는 가장 오래 걸리는 간선 정렬 작업의 시간 복잡도가 O(ElogE)이기 때문이다.