programmers.co.kr/learn/courses/30/lessons/42861#
문제 설명
n개의 섬 사이에 다리를 건설하는 비용(costs)이 주어질 때, 최소의 비용으로 모든 섬이 서로 통행 가능하도록 만들 때 필요한 최소 비용을 return 하도록 solution을 완성하세요.
다리를 여러 번 건너더라도, 도달할 수만 있으면 통행 가능하다고 봅니다. 예를 들어 A 섬과 B 섬 사이에 다리가 있고, B 섬과 C 섬 사이에 다리가 있으면 A 섬과 C 섬은 서로 통행 가능합니다.
제한사항
- 섬의 개수 n은 1 이상 100 이하입니다.
- costs의 길이는 ((n-1) * n) / 2이하입니다.
- 임의의 i에 대해, costs[i][0] 와 costs[i] [1]에는 다리가 연결되는 두 섬의 번호가 들어있고, costs[i] [2]에는 이 두 섬을 연결하는 다리를 건설할 때 드는 비용입니다.
- 같은 연결은 두 번 주어지지 않습니다. 또한 순서가 바뀌더라도 같은 연결로 봅니다. 즉 0과 1 사이를 연결하는 비용이 주어졌을 때, 1과 0의 비용이 주어지지 않습니다.
- 모든 섬 사이의 다리 건설 비용이 주어지지 않습니다. 이 경우, 두 섬 사이의 건설이 불가능한 것으로 봅니다.
- 연결할 수 없는 섬은 주어지지 않습니다.
입출력 예
n | costs | return |
4 | [[0,1,1],[0,2,2],[1,2,5],[1,3,1],[2,3,8]] | 4 |
입출력 예 설명
costs를 그림으로 표현하면 다음과 같으며, 이때 초록색 경로로 연결하는 것이 가장 적은 비용으로 모두를 통행할 수 있도록 만드는 방법입니다.
문제 해석
문제 설명을 읽어보면 의도를 알 수 있습니다. 문제에서 요구하는 바는 다리 건설에 드는 비용이 가장 작게 만들고, 사이클은 형성되지 않아도 됩니다. 자료구조, 알고리즘 때 익혔던 MST, Minimum Spanning Tree가 생각나는 문제입니다. 우리는 Edge의 형태가 필요한 게 아니고 Cost만 계산하면 되기 때문에 형태는 더욱 간단해집니다.
MST를 만드는 방법
1. Kruskal Algorithm
Kruskal Algorithm의 과정은 다음과 같습니다.
- 그래프의 Edge를 Cost(Weight)가 작은 순서대로 정렬합니다.
- 정렬된 Edge를 순회하며 해당 Edge를 MST에 포함했을 때 사이클을 형성하는지 체크합니다.
- 사이클을 형성하지 않는 Edge를 MST에 포함합니다.
정렬은 각 언어에서 기본으로 제공하며, Edge를 MST에 포함하는 방법도 간단합니다. 사이클을 형성하는지 체크하는 부분이 가장 중요하며, 이를 확인하기 위해 union-find 함수를 이용합니다.
def union(root1, root2):
global disjunctionSet
disjunctionSet[root2] = root1
for i in range(len(disjunctionSet)):
if disjunctionSet[i] == root2:
disjunctionSet[i] = root1
def find(vertex):
global disjunctionSet
if disjunctionSet[vertex] == -1:
return vertex
return disjunctionSet[vertex]
union 함수는 두 트리를 하나로 합치는 역할을 합니다. root2가 root1에 종속되며, root2를 가리키던 모든 요소를 root1을 가리키도록 변경합니다. union 함수를 통해 집합에 포함될 때 트리의 depth를 늘리지 않고, 최상단 노드에 포함되도록 만들었습니다.
find 함수는 자신이 속한 트리의 최상단 노드 값을 반환합니다. union 함수의 형식 상 depth가 최대 2이기 때문에 O(1)로 바로 찾아낼 수 있습니다.
2. Prim Algorithm
Prim Algorithm은 시작점을 지정한 후 출발하여 트리를 단계적으로 확장하는 방법이며, 과정은 다음과 같습니다.
- 시작점을 지정하여 트리에 포함합니다.
- 인접점 중 트리에 포함되지 않았으며, 가장 Cost가 낮은 Edge로 이어진 점을 선택하여 트리에 포함합니다.
- 트리가 n-1 개의 Edge를 가질 때까지 반복합니다.
- 위 과정을 시작점을 변경해가며 모든 점에 대해 반복합니다.
참고사항
Kruskal Algorithm의 시간 복잡도는 Edge를 정렬하는 시간에 좌우되며, 가장 효율적인 경우 O(E log E)가 됩니다. (E = number of Edge) 반면 Prim Algorithm의 시간 복잡도는 O(n^2), n = number of vertex이며, 문제와 같이 그래프에 Edge의 수가 ((n-1) * n) / 2개 이하로 존재하는 경우에는 Kruskal Algorithm이 더욱 유리합니다.
풀이
def union(root1, root2):
global disjunctionSet
disjunctionSet[root2] = root1
for i in range(len(disjunctionSet)):
if disjunctionSet[i] == root2:
disjunctionSet[i] = root1
def find(vertex):
global disjunctionSet
if disjunctionSet[vertex] == -1:
return vertex
return disjunctionSet[vertex]
def solution(n, costs):
global disjunctionSet
disjunctionSet = [-1 for _ in range(n)]
answer = 0
costs.sort(key=lambda x: x[-1])
for v1, v2, cost in costs:
root1 = find(v1)
root2 = find(v2)
if root1 == root2:
continue
union(root1, root2)
answer += cost
return answer
Kruskal Algorithm으로 구현한 풀이입니다. 최초 disjunctionSet은 모든 노드가 root인 경우로 생각하여 -1로 초기화합니다. costs를 비용을 기준으로 정렬한 뒤 루프를 수행합니다. root1과 root2가 동일한 경우에는 cycle이 형성되었다는 의미이므로 넘어갑니다. 그렇지 않은 경우에는 union을 수행하고, cost를 증가시켜 줍니다.
'알고리즘' 카테고리의 다른 글
[프로그래머스] 입국심사 [이분탐색, Binary Search] (0) | 2021.05.05 |
---|---|
[프로그래머스] 정수 삼각형 [동적 계획법] (0) | 2021.05.05 |
[프로그래머스] 등굣길 [동적계획법] (0) | 2021.05.04 |
[프로그래머스] 괄호 회전하기 (0) | 2021.05.04 |
[프로그래머스] 행렬 테두리 회전하기 [2021 Dev-Matching: 웹 백엔드 개발] (0) | 2021.05.04 |