알고리즘 입문
개념 배우기직접 풀어보기
놀면 뭐 하니,
그냥 하는거야.

가장 싸게 다 잇자냥! 최소 신장 트리

코딩냥

안녕, 나 코딩냥이다냥! 도시들을 도로로 다 잇고 싶은데 비용을 최소로 하고 싶다냥. 이걸 푸는 게 최소 신장 트리(MST)라냥.

그리고 만드는 방법인 크루스칼은, 16장 유니온 파인드를 거의 그대로 가져다 쓴다냥. 배운 걸 바로 써먹는 장이라냥.

오늘의 학습 목표

  • 신장 트리와 최소 신장 트리가 뭔지 안다.
  • 크루스칼로 MST 를 만든다.
  • 연결이 안 되는 경우를 판별한다.
  • MST 의 가장 비싼 간선을 활용한다.
  • 이미 놓인 간선이 있을 때 응용한다.

1단계 · 신장 트리가 뭐냥 (8분)

코딩냥

신장 트리는 그래프의 모든 점을 사이클 없이 하나로 잇는 트리라냥. 간선을 딱 n-1개만 쓴다냥. 그중 비용 합이 가장 작은 것이 최소 신장 트리라냥.

비유로 이해하기 — 마을에 수도관 깔기

집 n채를 수도관으로 다 이어야 한다냥. 모든 집에 물이 가되, 관 놓는 비용은 최소로 하고 싶다냥.

고리(사이클)를 만들면 낭비라냥 — 이미 이어진 집을 또 잇는 거니까라냥. 그래서 사이클 없이, 딱 n-1개의 관으로, 가장 싸게 잇는 게 MST 라냥.

2단계 · 크루스칼 (13분)

코딩냥

방법은 놀랄 만큼 단순하다냥. 싼 간선부터 골라서, 사이클을 만들지 않으면 쓰고, 만들면 버린다냥. 그게 전부라냥.

뼈대정렬하고, 사이클 없는 것만 더한다냥
  1. 모든 간선을 비용 오름차순으로 정렬한다
  2. 싼 것부터 하나씩 본다
  3. 두 끝이 다른 무리면 쓴다 (합친다)
  4. 같은 무리면 버린다 (사이클이 되니까)
  5. n-1개를 쓰면 끝이라냥
edges.sort()          # (비용, u, v) 순으로 정렬한다냥

parent = list(range(n + 1))
def find(x):
    while parent[x] != x:
        parent[x] = parent[parent[x]]
        x = parent[x]
    return x

total = 0
for w, u, v in edges:
    ru, rv = find(u), find(v)
    if ru != rv:          # 다른 무리면 잇는다냥
        parent[rv] = ru
        total += w

16장의 유니온 파인드가 그대로라냥.

"두 끝이 같은 무리냥?" 이 바로 16장의 사이클 판별이라냥. find(u) != find(v) 면 잇고, 같으면 버린다냥. 크루스칼은 정렬 + 유니온 파인드일 뿐이라냥.

정렬을 빼먹으면 최소가 아니다냥.

싼 간선부터 봐야 최소 비용이 된다냥. 정렬 없이 입력 순서대로 더하면 그냥 아무 신장 트리일 뿐, 최소가 아니라냥. 오늘 1번 문제가 이걸 잡는다냥.

3단계 · 연결이 안 될 때 (10분)

코딩냥

간선이 모자라면 하나로 못 잇는다냥. 섬처럼 따로 노는 무리가 남는 거라냥. 이걸 어떻게 알아채냥?

쓴 간선이 정확히 n-1개인지 세면 된다냥.

신장 트리는 간선이 딱 n-1개라냥. 크루스칼이 끝났는데 쓴 간선이 그보다 적으면, 모두를 잇지 못한 거라냥.

used = 0
# ... 간선을 쓸 때마다 used += 1
print(total if used == n - 1 else -1)

오늘 2번 문제가 이거라냥. 연결이 안 되면 -1 을 출력한다냥.

"쓴 간선 수" 대신 "남은 무리 수"를 세도 된다냥. 크루스칼이 끝난 뒤 대표가 자기 자신인 노드가 하나뿐이면 다 이어진 거라냥.

4단계 · 가장 비싼 간선 활용 (12분)

코딩냥

크루스칼은 싼 것부터 더하니, MST 에 마지막으로 들어간 간선이 가장 비싼 간선이라냥. 이 사실이 은근히 쓸모가 많다냥.

응용가장 비싼 간선을 끊으면 두 그룹이라냥

MST 는 트리라, 간선 하나를 끊으면 정확히 두 조각으로 갈라진다냥. 그중 가장 비싼 간선을 끊으면, 두 그룹으로 나누면서 유지 비용을 가장 많이 아낀다냥.

그래서 "모두 잇되 두 그룹으로 나누는 최소 유지 비용" 은 —

MST 전체 비용 - MST 의 가장 비싼 간선

오늘 3번(가장 비싼 간선)과 4번(두 그룹 나누기)이 이 아이디어라냥.

mx = 0
for w, u, v in edges:
    ru, rv = find(u), find(v)
    if ru != rv:
        parent[rv] = ru
        total += w
        mx = w          # 마지막에 쓴 게 가장 비싼 간선이라냥

간선을 오름차순으로 보니, MST 에 실제로 쓴 간선 중 마지막이 가장 비싸다냥. 그냥 mx = w 로 계속 덮어쓰면 자동으로 최댓값이 남는다냥.

5단계 · 이미 놓인 간선 (10분)

코딩냥

현실에서는 이미 놓인 도로가 있을 수 있다냥. 그건 공짜로 쓰면 된다냥. 이때도 16장의 아이디어를 쓴다냥.

응용먼저 합쳐두고 크루스칼이라냥

이미 이어진 쌍들을 먼저 union 해둔다냥. 그러면 그 무리들은 이미 하나로 묶여 있어서, 크루스칼이 그 위에서 나머지만 최소로 잇는다냥.

for a, b in built:        # 이미 놓인 것부터 합친다냥
    parent[find(a)] = find(b)

# 이제 남은 후보 간선으로 크루스칼을 돌린다냥

이미 놓인 간선으로 무리가 줄었으니, 필요한 추가 간선도 그만큼 줄어든다냥. "쓴 간선 수 == (남은 무리 수) - 1" 이면 다 이어진 거라냥.

오늘 5번 문제가 이거라냥. 16장의 union 을 크루스칼 앞에 붙이면 끝이라냥.

오늘 배운 내용 정리

코딩냥

먼저 스스로 답해보고, "정답 보기"를 눌러서 맞는지 확인해보자냥!

Q1.최소 신장 트리가 뭐냥?
Q2.크루스칼은 어떻게 MST 를 만드냥?
Q3.크루스칼에서 정렬을 빼먹으면 어떻게 되냥?
Q4.그래프가 연결됐는지 어떻게 아냥?
Q5.모두 잇되 두 그룹으로 나누는 최소 유지 비용은 어떻게 구하냥?

이제 직접 풀어볼 차례다냥

코딩냥

오늘 문제는 다섯 개라냥. 1번 MST 비용, 2번 연결 판별(-1), 3번 가장 비싼 간선, 4번 두 그룹 나누기, 5번 이미 놓인 간선이라냥.

전부 정렬 + 유니온 파인드 한 뼈대의 변주라냥. 크루스칼을 한 번 제대로 짜두면 나머지는 한두 줄 차이라냥. 가보자냥~