최소 스패닝 트리 (MST) - 크루스칼

이번 포스팅에서는 최소 스패닝 트리에 대해 다루고, 구현하는 방법에 대해 코드와 함께 설명하도록 한다.

스패닝 트리

  • 스패닝 트리가 무엇일까? 스패닝 트리에 대한 정의를 먼저 정확히 내리면서 글을 시작해보자.
  • 무향 그래프의 스패닝 트리는 원래 그래프의 모든 정점과 간선의 부분 집합으로 구성된 부분 그래프이다.
  • 트리는 그래프의 일종이므로, 부분 그래프라는 말이 이해가 갈 것이다.
  • 이때 이 부분 그래프, 즉 스패닝 트리에 포함된 간선들은 정점들을 트리 형태로 전부 연결해야 한다.
  • 즉, 간선들이 사이클을 이루지 않는다는 말이다!

  • 왼쪽 그림은 올바른 스패닝 트리이다. 모든 정점으로 이루어져 있고, 굵은 간선이 사이클을 이루지 않는다.
  • 반면, 오른쪽 그림을 스패닝 트리가 아니다. 사이클도 아니고, 그래프가 하나로 연결되어 있지 않다.

최소 스패닝 트리란?

  • 하나의 그래프에서 스패닝 트리는 여러개가 나올 수 있다.
  • 특히, 그래프 중에서 가중치 그래프의 스패닝 트리중에 가중치의 합이 가장 작은 트리가 최소 스패닝 트리(Minimum Spanning Tree)이다.
  • 최소 스패닝 트리를 구하기 위한 2가지 알고리즘은 모두 그리디를 바탕으로 이루어져있다.

크루스칼 알고리즘이란?

  • 크루스칼 알고리즘은 위에서 말한 것처럼 최소 스패닝 트리를 찾기 위해 그리디하게 가중치가 작은 것부터 추가해 나가는 알고리즘이다.
  • 모든 간선을 가중치의 오름차순으로 정렬한 뒤, 스패닝 트리에 하나씩 추가해 나간다.
  • 이때, 사이클을 이루면 스패닝 트리가 아니므로, 사이클이 생기는 간선은 제외한다.
  • 즉, 가중치가 작은 것부터 하나씩 추가해나가면서 사이클 여부를 확인하는 것이 이 알고리즘의 핵심이다.
  • 사이클 생성 확인은 어떻게 할까?

크루스칼 - 사이클 확인

  • 첫번째로 사이클을 확인하는 방법은 간선을 하나 추가할 때마다 dfs를 진행하며 백엣지 유무를 확인한다.
  • 이 경우는 간선 하나마다(O(E)) dfs(O(N+E))를 진행하므로, O(E^2)의 시간복잡도를 가지게 된다.
  • 두번째 방법은 만약 간선을 추가했을 때, 사이클이 생긴다면, 간선의 양 끝 노드가 이미 같은 집합에 속하게 된다.
  • 즉, 둘이 같은 집합인지 확인하는 연산과 같은 집합으로 합치는 연산을 빠르게 할 수 있으면 시간복잡도를 줄일 수 있다.
  • 결국 유니온 파인드이다!

크루스칼 - 코드 작성 아이디어

  • 모든 노드를 처음에는 다른 집합에 속해있게 둔다.
  • 1번노드와 2번노드의 간선을 연결할 때, 둘은 초기에 다른 집합에 있으므로, 유니온 연산을 해서 같은 집합에 있게 만든다.
  • 2번노드와 3번노드의 간선을 선택한다하면, 역시 2번노드 집합에는 1,2만 있고, 3번 노드 집합은 3번만 있으므로 둘이 초기에 다른 집합에 속해있고, 유니온을 시킨다.
  • 이후 만약 1번 노드와 3번노드의 간선을 선택하면, 둘은 이미 같은 집합에 속해있으므로 사이클이 생성된다. 다음 간선으로 넘어가면 된다!
  • 이렇게 간선을 선택해나가며, 트리의 간선의 개수인 N-1개까지 선택을 하면 최소 스패닝 트리가 완성이 된다!
  • 유니온 파인드 연산의 시간복잡도는 상수시간과 다름없으므로, 트리를 만드는 반복문의 시간복잡도는 O(E)가 된다.
  • 결국 전체 시간 복잡도는, 간선을 가중치의 오름차순을 기준으로 정렬을 하기 때문에, O(ElogE), 즉 간선의 개수에 의존적인 시간복잡도를 가지게 된다.
import java.util.*;
import java.io.*;

/**
 * @Author by Sangwoo
 * @Part MST using Kruskal
 */
public class MST_Kruskal {
    private static int V, E;
    private static PriorityQueue<Edge> pq = new PriorityQueue<>((o1, o2) -> Integer.compare(o1.e, o2.e));

    private static class Edge{
        int u, v, e;
        Edge(int u, int v, int e) {
            this.u = u;
            this.v = v;
            this.e = e;
        }
    }

    private static class DisjointSet{
        int[] p;
        DisjointSet() {
            this.init();
        }
        private void init() {
            p = new int[V + 1];
            for (int i = 1; i <= V; i++) {
                p[i] = i;
            }
        }
        private int find(int n) {
            if (n == p[n]) return n;
            return p[n] = find(p[n]);
        }
        private void merge(int n, int m) {
            n = find(n);
            m = find(m);
            p[m] = n;
        }
    }

    public static void main(String[] args) throws IOException {
        input();
        int n = 0;
        int ans = 0;

        // make DisjointSet Instance
        DisjointSet ds = new DisjointSet();

        // loop
        while (!pq.isEmpty() && n <= V - 1) {
            Edge edge = pq.poll();
            if (ds.find(edge.u) != ds.find(edge.v)) {
                ans += edge.e;
                n += 1;
                ds.merge(edge.u, edge.v);
            }
        }
        System.out.println(ans);
    }

    // Input
    private static void input() throws IOException {
        BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(input.readLine());
        V = Integer.parseInt(st.nextToken());
        E = Integer.parseInt(st.nextToken());
        for (int i = 0; i < E; i++) {
            st = new StringTokenizer(input.readLine());
            int n = Integer.parseInt(st.nextToken());
            int m = Integer.parseInt(st.nextToken());
            int w = Integer.parseInt(st.nextToken());
            pq.add(new Edge(n, m, w));
        }
    }
}

정당성 증명 - 귀류법

  • 크루스칼이 선택하는 간선 중 최소 신장 트리 T에 포함되지 않는 간선이 있다고 하자. (u-v)
  • T는 이 간선을 포함하지 않기 때문에, u와 v는 다른 경로로 연결되어 있다.
  • 이 경로에는 (u-v)보다 크거나 같은 간선이 무조건 존재한다.
  • 왜냐면, (u-v)보다 모두 작으면, 애초에 (u-v)를 크루스칼이 선택할 일이 없다.
  • 이때 이 경로(T에 속해있는) 간선을 하나 지우고 (u-v)를 연결해도 여전히 스패닝 트리이다.
  • 그렇다면, (u-v)가 선택된 스패닝 트리의 가중치는 기존 T보다 작거나 같을 것이다.
  • T를 최소 스패닝 트리라고 했으므로, (u-v)가 포함된 스패닝 트리도 최소 스패닝 트리가 됨을 알 수 있다.

댓글남기기