9. 그래프이론 - 신장 트리

728x90

공부할 내용

  1. 서로소 집합
  2. 신장 트리
    • 크루스칼 알고리즘
  3. 위상 정렬 (해당 주제는 앞에서 정리했기 때문에 따로 정리하지 않아도 된다.)

공부 방법

신장 트리

  • 그래프에서 모든 노드를 포함하면서 사이클이 존재하지 않는 부분 그래프를 의미한다.
    • 모든 노드가 포함되어 서로 연결되면서 사이클이 존재하지 않는다는 조건은 트리의 조건이기도 하다.
    • 원본 그래프에 존재하는 간선을 모두 활용하지 않고 일부 간선만 활용하여 모든 노드가 포함되어있는 하나의 부분 그래프를 만드는 것이다.
      image

image

신장트리는 모든 노드가 다 연결되어있지만, 일부 간선을 사용하지 않아도 괜찮다는 점에서 실제 문제 상황에서도 이런 아이디어가 효과적으로 사용될 수 있는 경우가 존재한다.

최소 신장 트리

  • 최소한의 비용으로 구성되는 신장 트리를 찾아야 할 때 어떻게 해야할까?
  • 예를 들어 N개의 도시가 존재하는 상황에서 두 도시 사이에 도로를 놓아 전체 도시가 서로 연결될 수 있게 도로를 설치하는 경우를 생각해보자.
    • 두 도시 A, B를 선택했을 때 A에서 B로 이동하는 경로가 반드시 존재하도록 도로를 설치한다.

image

크루스칼 알고리즘

  • 대표적인 최소 신장 트리 알고리즘이다.
  • 그리디 알고리즘으로 분류된다.
  • 구체적인 동작 과정은 다음과 같다.
    1. 간선 데이터를 비용에 따라 오름차순으로 정렬한다.
    2. 간선을 하나씩 확인하며 현재의 간선이 사이클을 발생시키는지 확인한다.
      1) 사이클이 발생하지 않는 경우 최소 신장 트리에 포함시킨다.
      2) 사이클이 발생하는 경우 최소 신장 트리에 포함시키지 않는다.
    3. 모든 간선에 대하여 2번의 과정을 반복한다.

ex. 아래와 같이 7개의 노드가 존재하는 경우를 가정하자.

  • [초기 단계] 그래프의 모든 간선 정보에 대하여 **오름차순 정렬을 수행한다.

지금 예시의 테이블은 가독성을 위해 간선에 따라서 나열했지만, 실제 크루스칼 알고리즘은 비용을 오름차순으로 정렬하는 작업이 우선적으로 수행되어야한다.
image

참고로 최종적으로 만들어지는 최소신장트리에 포함되어있는 간선의 갯수는 전체 노드의 개수 - 1이다. 이는 기본적으로 트리가 가지는 특징이고 사이클 또한 존재하지 않는다는 특징이 있다.

  • [Step 1] 아직 처리하지 않은 간선 중에서 가장 짧은 간선인 (3, 4)를 선택하여 처리한다.

image

  • [Step 2] 아직 처리하지 않은 간선 중에서 가장 짧은 간선인 (4, 7)을 선택하여 처리한다.

image

  • [Step 3] 아직 처리하지 않은 간선 중에서 가장 짧은 간선인 (4, 6)을 선택하여 처리한다.

image

  • [Step 4] 아직 처리하지 않은 간선 중에서 가장 짧은 간선인 (6, 7)을 선택하여 처리한다.

    이미 같은 집합에 속해있기 때문에 해당 간선은 무시한다.

  • [Step 5] 아직 처리하지 않은 간선 중에서 가장 짧은 간선인 (1, 2)을 선택하여 처리한다.

image

  • [Step 6] 아직 처리하지 않은 간선 중에서 가장 짧은 간선인 (2, 6)을 선택하여 처리한다.

image

  • [Step 7] 아직 처리하지 않은 간선 중에서 가장 짧은 간선인 (2, 3)을 선택하여 처리한다.

    이미 같은 집합에 속해있기 때문에 해당 간선은 무시한다.

  • [Step 8] 아직 처리하지 않은 간선 중에서 가장 짧은 간선인 (5, 6)을 선택하여 처리한다.

image

  • [Step 9] 아직 처리하지 않은 간선 중에서 가장 짧은 간선인 (1, 5)을 선택하여 처리한다.

    이미 같은 집합에 속해있기 때문에 해당 간선은 무시한다.

  • [알고리즘 수행 결과]

    • 최소 신장 트리에 포함되어 있는 간선의 비용만 모두 더하면, 그 값이 최종 비용에 해당한다.

image

public class Kruskal {
    // 노드의 개수(V)와 간선(Union 연산)의 개수(E)
    // 노드의 개수는 최대 100,000개라고 가정
    private static int v, e;
    private static int[] parent = new int[1000001]; // 부모 테이블 초기화하기

    // 모든 간선을 담을 리스트와, 최종 비용을 담을 변수
    private static List<Edge> edges = new ArrayList<>();
    private static int result = 0;

    // 특정 원소가 속한 집합을 찾기
    private static int findParent(int x) {
        // 루트 노드가 아니라면, 루트 노드를 찾을 때까지 재귀적으로 호출
        if (x != parent[x]) {
            parent[x] = findParent(parent[x]);
        }

        return parent[x];
    }

    // 두 원소가 속한 집합을 합치기
    private static void unionParent(int a, int b) {
        a = findParent(a);
        b = findParent(b);
        if (a < b) {
            parent[b] = a;
            return;
        }

        parent[a] = b;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        v = sc.nextInt();
        e = sc.nextInt();

        // 부모 테이블상에서, 부모를 자기 자신으로 초기화 
        for (int i = 1; i <= v; i++) {
            parent[i] = i;
        }

        // 모든 간선에 대한 정보를 입력 받기
        for (int i = 0; i < e; i++) {
            int a = sc.nextInt();
            int b = sc.nextInt();
            int cost = sc.nextInt();
            edges.add(new Edge(cost, a, b));
        }

        // 간선을 비용순으로 정렬
        Collections.sort(edges);

        // 간선을 하나씩 확인하며
        for (int i = 0; i < edges.size(); i++) {
            int cost = edges.get(i).getDistance();
            int a = edges.get(i).getNodeA();
            int b = edges.get(i).getNodeB();

            // 사이클이 발생하지 않는 경우에만 집합에 포함
            if (findParent(a) == findParent(b)) {
                continue;
            }

            unionParent(a, b);
            result += cost;
        }

        System.out.println(result);
    }

    static class Edge implements Comparable<Edge> {
        private int distance;
        private int nodeA;
        private int nodeB;

        public Edge(int distance, int nodeA, int nodeB) {
            this.distance = distance;
            this.nodeA = nodeA;
            this.nodeB = nodeB;
        }

        public int getDistance() {
            return distance;
        }

        public int getNodeA() {
            return nodeA;
        }

        public int getNodeB() {
            return nodeB;
        }

        // 거리(비용)가 짧은 것이 높은 우선순위를 가지도록 설정
        @Override
        public int compareTo(Edge other) {
            if (this.distance < other.distance) {
                return -1;
            }
            return 1;
        }
    }
}

크루스칼 알고리즘 성능 분석

  • 크루스칼 알고리즘은 간선의 개수가 E개일 때, O(ElogE)의 시간 복잡도를 가진다.
  • 크루스칼 알고리즘에서 가장 많은 시간을 요구하는 곳은 간선을 정렬을 수행하는 부분이다.
    • 표준 라이브러리를 이용해 E개의 데이터를 정렬하기 위한 시간 복잡도는 O(ElogE)이다.
728x90