본문 바로가기
알고리즘, 자료구조/크루스칼 알고리즘

크루스칼 알고리즘(Kruskal Algorithm) [Python / 파이썬]

by Deeppago 2022. 1. 15.

1. 크루스칼 알고리즘

신장 트리란 하나의 그래프가 있을 때 모든 노드를 포함하면서 사이클이 존재하지 않는 부분 그래프를 의미한다.

 

우리는 다양한 문제 상황에서 가능한 한 최소한의 비용으로 신장 트리를 찾아야 할 때가 있다. 예를 들어 N개의 도시가 존재하는 상황에서 두 도시 사이에 도로를 놓아 전체 도시가 서로 연결될 수 있게 도로를 설치하는 경우를 생각해보자. 2개의 도시 A, B를 선택했을 때, 도시 A에서 도시 B로 이동하는 경로가 반드시 존재하도록 도로를 설치하고자 한다. 모든 도시를 '연결'할 때, 최소한의 비용으로 연결 하려면 어떤 알고리즘을 이용해야 할까?

 

 

예를들어 위와같은 그래프가 있다고 가정해보자. 3개의 도시가 있고, 각 도시 간 도로를 건설하는 비용은 23, 13, 25이다.

여기서 노드 1, 2, 3을 모두 연결하기 위해서 가장 최소한의 비용을 가지는 신장 트리는 도시 1과 3을 연결하는 23 + 도시 3과 2를 연결하는 13 총 36이다. 

 

이처럼 신장 트리 중에서 최소 비용으로 만들 수 있는 신장 트리를 찾는 알고리즘을 '최소 신장 트리 알고리즘'이라고 한다.  크루스칼 알고리즘은 이러한 최소 신장 트리 알고리즘의 대표적인 알고리즘이다. 

 

크루스칼 알고리즘은 그리디 알고리즘으로 분류된다. 먼저 모든 간선에 대하여 정렬을 수행한 뒤에 가장 거리가 짧은 간선부터 집합에 포함시키면 된다. 이렇게 하면 항상 최적의 해를 보장할 수 있다. 이때 사이클을 발생시킬 수 있는 간선의 경우, 집합에 포함시키지 않는다. 구체적인 알고리즘은 다음과 같다.

 

  • 1. 간선 데이터를 비용에 따라 오름차순으로 정렬한다.
  • 2. 간선을 하나씩 확인하며 현재의 간선이 사이클을 발생시키는지 확인한다.
    • 사이클이 발생하지 않는 경우 최소 신장 트리에 포함시킨다.
    • 사이클이 발생하는 경우 최소 신장 트리에 포함시키지 않는다.
  • 3. 모든 간선에 대해서 2번의 과정을 반복한다. 

크루스칼 알고리즘은 이전에 업로드 하였던 서로소 집합 알고리즘을 사용하여 구현하므로 간선을 하나씩 확인하면서 두 노드가 포함되어 있는 집합을 합치는 과정을 반복하면서 사이클을 판별할 수 있다. 사이클 판별 알고리즘은 다음과 같다.

  • 각 간선을 확인하며 두 노드의 루트 노드를 확인한다.
    • 루트 노드가 서로 다르다면 두 노드에 대하여 union연산을 수행한다.
    • 루트 노드가 서로 같다면 사이클(Cycle)이 발생한 것인다.

위와 같이 서로소 집합 알고리즘은 무방향 그래프에서의 사이클을 판별할 수 있다.

1.1 구현 소스 코드

아래 코드는 간선들을 입력받아 최소 신장 트리를 생성하고, 그때의 간선들의 총합을 구하는 코드이다.

 

# 특정 원소가 속한 집합을 찾기
def find_parent(parent, x):
    if parent[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())
parent = [0] * (v + 1)

# 부모 테이블상에서, 부모를 자기 자신으로 초기화
for i in range(1, v + 1):
    parent[i] = i

edges = []
result = 0

# 간선을 입력받아 coat를 기준으로 오름차순 정렬
for _ 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)

'''
[Input Example 1]
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
[Output Example 1]
159
'''

 


2. 크루스칼 알고리즘의 시간 복잡도

크루스칼 알고리즘은 간선의 개수가 E개일 때, O(ElogE)의 시간 복잡도를 가진다. 왜냐하면 크루스칼 알고리즘에서 시간이 가장 오래 걸리는 부분이 간선을 정렬하는 작업이며, E개의 데이터를 정렬했을 때의 시간 복잡도는 O(ElogE)이기 때문이다. 크루스칼 내부에서 사용되는 서로소 집합 알고리즘의 시간 복잡도는 정렬 알고리즘의 시간 복잡도보다 작으므로 무시한다.

댓글