(최소 스패닝 트리)
# 문제
그래프가 주어졌을 때, 그 그래프의 최소 스패닝 트리를 구하는 프로그램을 작성하시오.
최소 스패닝 트리는, 주어진 그래프의 모든 정점들을 연결하는 부분 그래프 중에서 그 가중치의 합이 최소인 트리를 말한다.
# 입력
첫째 줄에 정점의 개수 V(1 ≤ V ≤ 10,000)와 간선의 개수 E(1 ≤ E ≤ 100,000)가 주어진다. 다음 E개의 줄에는 각 간선에 대한 정보를 나타내는 세 정수 A, B, C가 주어진다. 이는 A번 정점과 B번 정점이 가중치 C인 간선으로 연결되어 있다는 의미이다. C는 음수일 수도 있으며, 절댓값이 1,000,000을 넘지 않는다.
그래프의 정점은 1번부터 V번까지 번호가 매겨져 있고, 임의의 두 정점 사이에 경로가 있다. 최소 스패닝 트리의 가중치가 -2,147,483,648보다 크거나 같고, 2,147,483,647보다 작거나 같은 데이터만 입력으로 주어진다.
# 출력
첫째 줄에 최소 스패닝 트리의 가중치를 출력한다.
먼저 스패닝 트리가 무엇인지 알아보겠습니다!
신나는 나무
스패닝 트리는 다음 조건이 모두 충족되는 경우 트리입니다.
- 모든 노드가 연결되어 있습니다.
- 주기가 존재하지 않습니다.
스패닝 트리의 올바른 예
비신장 트리의 예
연결되지 않은 노드 또는 주기가 있는 경우 트리가 확장되지 않습니다.
최소 스패닝 트리 찾기
- 최소 스패닝 트리 알고리즘: 스패닝 트리 중 최소 비용빌드할 스패닝 트리를 찾는 알고리즘입니다.
최소한의 비용으로 스패닝 트리를 찾는 알고리즘, 크루스칼 알고리즘알아 보자!
크루스칼 알고리즘
- Kruskal 알고리즘은 대표적인 최소 스패닝 트리 알고리즘이다.
1) 모든 간선을 비용 오름차순으로 정렬한다.
2) 비용이 적은 간선을 가져온다.
3) 이 간선이 연결되면 사이클을 발생시키는지 확인한다.
4) 사이클을 발생시키지 않는다면, 이 간선을 신장 트리에 포함시킨다.
~반복~
이는 항상 최적의 솔루션을 보장합니다.
1. 비용의 오름차순으로 모든 가장자리를 정렬합니다.
edges = () # 간선
for _ in range(E):
a, b, cost = map(int, sys.stdin.readline().split())
edges.append((cost, a, b))
edges.sort() # 비용순 오름차순 정렬
2. 유리한 이점을 얻으십시오.
- 1단계에서 오름차순으로 정렬해 놓았으니 하나씩 빼주시면 됩니다!
for edge in edges:
cost, a, b = edge # cost 오름차순으로 정렬되어 있음
3. 이 에지가 연결되면 주기가 발생하는지 확인하십시오.
이 에지가 연결될 때 순환을 일으키는지 확인하는 방법,
주어진 모서리의 모든 정점 상위 노드알아보는 것입니다
(각 노드의 부모를 찾아 두 부모가 같으면 그 가장자리가 연결될 때 순환을 일으킬 것입니다!!!)
왜,,주기가 뭐야,,,
연결된 노드가 같은 노드에 연결되면 뒤집어서 서로 만나는 것이 바로 싸이클~!
부모 노드는 각 노드입니다. 가장 작은 연결된 노드수단
이 부모 노드가 동일한 노드를 연결하면 주기가 발생합니다.
위 그림에서 점선으로 표시된 부분입니다. 6-7
가장자리가 연결될 때 주기가 발생합니까?
- 7에 연결된 가장 작은 노드는 3이고,
- 6에 연결된 가장 작은 노드는 3입니다.
둘 다 3으로 이어집니다. 다시 말해서, 6-7
에지가 연결되면 주기가 트리거됩니다.
그럴땐 연결하면 안됩니다. 싸이클이 발생하면 스패닝 트리가 아닙니다~!
이제 부모 노드를 먼저 찾아야 합니다.
2-1) 부모 테이블을 생성합니다.
- 인덱스를 노드로 설정하고 값을 부모 노드로 설정합니다.
parent(노드) = 부모노드
- 노드는 1부터 지정되므로 상위 테이블의 크기는 노드 수 + 1입니다.
(인덱스 0을 사용하지 않기 때문입니다!) - 먼저 자체적으로 초기화합니다.
(노드 1 부모는 1, 노드 2 부모는 2…)
# 부모를 담을 테이블 (parent(노드) = 부모노드)
parent = (0) * (V + 1)
# 부모를 자기 자신으로 초기화
for i in range(1, V+1):
parent(i) = i
2-2) 부모 노드가 동일한지 확인합니다.
이것은 두 번째 for 문으로 이어집니다.
- 비용이 가장 낮은 에지를 선택하고 해당 에지에 연결된 모든 노드에 동일한 부모 노드가 있는지 확인합니다.
(같은 부모는 같은 집합에 속하는 것을 의미합니다. 같은 집합에 속할 필요는 없습니다.)
for edge in edges:
cost, a, b = edge # cost 오름차순으로 정렬되어 있음
if find_parent(a) != find_parent(b): # 같은 집합이 아니면,
- 아래는 부모를 찾는 기능입니다.
함수를 재귀적으로 호출하고 부모 테이블의 값을 즉시 업데이트(경로 압축 기술)하여 시간 복잡도를 개선했습니다.
# 부모 찾는 함수
def find_parent(x):
if parent(x) != x:
parent(x) = find_parent(parent(x))
return parent(x)
4. 주기가 발생하지 않으면 스패닝 트리에 에지를 포함합니다.
2 또는 3은 지침을 따릅니다.
result = 0
for edge in edges:
cost, a, b = edge # cost 오름차순으로 정렬되어 있음
if find_parent(a) != find_parent(b): # 같은 집합이 아니면,
union_parent(a, b) # 연결
result += cost # 연결된 것 cost 누적
- 비용 절감을 위한 변수
result
새로 선언했습니다. - 에지가 연결될 때마다 해당 에지의 비용은
result
이 값을 최종 결과로 출력합니다. - 실제로 트렁크를 연결하는 부분
union_parent(a, b)
이 함수 안에서 발생합니다.
스패닝 트리에 에지를 포함하는 기능
union_parent(a, b)
함수에 무엇이 있는지 봅시다!
def union_parent(a, b):
# 집합(부모 노드) 찾기
a = find_parent(a)
b = find_parent(b)
# 큰 거에서 작은 거로 연결
if a < b:
parent(b) = a
else:
parent(a) = b
연결할 두 노드의 부모를 찾아 연결합니다.
왜?
- 노드를 연결할 때 방향은 큰 노드에서 작은 노드로 향해야 하며 작은 노드가 큰 노드의 부모가 됩니다.
즉, 노드를 연결하려면 큰 노드의 부모 노드를 작은 노드 위에 놓으세요!! - 그런데,,,,
노드 3과 노드 2가 연결되면,
노드 2가 1에 연결되어 있으므로
노드 3의 부모 노드도 2가 아닌 1이 됩니다. - 따라서 Edge가 연결될 때마다 상위 노드 업데이트그것을해야합니다!
3과 2를 결합할 때 3 -> 2로 끝나는 것이 아니라 2의 부모를 찾아 3과 결합하여 3 -> 1로 변경해야 합니다!! - 부모노드를 연결하기 때문에 함수는 먼저 부모노드를 찾는다.
find_parent
호출하여 찾은
큰 부모 노드를 작은 부모 노드에 연결합니다. 즉, 부모 노드를 함께 연결합니다.
Self와 parent는 당연히 연결되어 있기 때문에 부모 노드만 연결하면 연결이 계속됩니다. - 이와 같이 연결되지 않은 것 중 가장 저렴한 것을 한두 개 추가하여 한 번에 하나씩 연결하여 최소 스패닝 트리를 완성한다.
끝!
전체 코드
import sys
V, E = map(int, input().split()) # 정점의 개수, 간선의 개수
# 부모를 담을 테이블 (parent(노드) = 부모노드)
parent = (0) * (V + 1)
# 부모를 자기 자신으로 초기화
for i in range(1, V+1):
parent(i) = i
# 부모 찾는 함수
def find_parent(x):
if parent(x) != x:
parent(x) = find_parent(parent(x))
return parent(x)
# 간선 연결하는 함수(집합 합치기)
def union_parent(a, b):
# 집합(부모 노드) 찾기
a = find_parent(a)
b = find_parent(b)
# 큰 거에서 작은 거로 연결
if a < b:
parent(b) = a
else:
parent(a) = b
edges = () # 간선
for _ in range(E):
a, b, cost = map(int, sys.stdin.readline().split())
edges.append((cost, a, b))
edges.sort() # 비용순 오름차순 정렬
result = 0
for edge in edges:
cost, a, b = edge # cost 오름차순으로 정렬되어 있음
if find_parent(a) != find_parent(b): # 같은 집합이 아니면,
union_parent(a, b) # 연결
result += cost # 연결된 것 cost 누적
print(result)