mst(최소스패닝트리)

mst(최소스패닝트리)

Featured image

최소신장트리는 ‘그래프에서 모든 정점을 연결하는 간선들의 가중치의 합이 최소가 되는 트리’를 뜻한다. MST(minimum spanning tree)의 줄임말이다. 한마디로 모든 정점을 연결하는데 간선의 cost가 최소가 될 때를 찾는 알고리즘이다. 그렇기 때문에 방향이 없는 그래프에서 사용된다. 사용되는 경우는 다음과 같다

mst가 사용되는 경우


신장트리

mst

MST 표현

MST 알고리즘은 Prim, Kruskal 2개가 있다. 프림, 크루스칼 알고리즘을 하나씩 살펴볼 건데 각각 어떤 차이가 있고 어떤 경우에 시간복잡도가 더 낮은지 한변 살펴보자 프림: O(ElogV) 간선 많을 때


Prim

핵심은 Priority Queue를 이용한 최소 합으로 구현하는 것이다. 한 정점에서 아직 연결되지 않은 정점들에 대해 간선을 pq에 추가해준다. 시간 복잡도는

모든 간선 E * 간선을 통해 삽입된 정점의 가중치 정렬 logV = O(ElogV)
프림 알고리즘은 간선의 수가 많을 때 더 효과적인 알고리즘이다.

Prim 알고리즘

Q <- G.V                    // 우선순위 Q에 모든 정점 넣는다

MST_PRIM(G, r)          // G: 그래프, r: 시작 정점
    result = 0, cnt = 0         // result: 신장트리 최소 비용,cnt: 처리한 정점 수, visited[]: 최소신장트리 구성에 포함된 정점여부
    FOR u in G.V
        minEdge[u] = INF        //minEdge[]: 각 정점기준으로 다른 정점과의 간선 중 최소비용
    minEdge[r] = 0              // 시작정점 r의 최소비용 0 처리

    WHILE Q != empty            // 빈 Q가 아닐동안 반복
        u = Extract_MIN(Q)      // 방문하지 않은(mst에 포함되지 않은 정점) 최소비용 정점 찾기
        visited[u] = true       // 방문처리
        result += minEdge[u]    // 비용누적
        if(++cnt == N)  break;  // 모든 정점이 다 연결되었으면 최소신장트리 완성
        FOR v in G.Adj[u]       // u의 인접 정점들 탐색
            IF !visited[v] AND w(u, v) < minEdge[v] // u에서 v로의 비용이 v의 최소비용보다 작다면 갱신
                minEdge[v] = w(u, v)

백준에서 MST 대표 문제인 1197번 최소 스패닝 트리를 Prim을 이용해 풀어보자. https://www.acmicpc.net/problem/1197

package boj;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.StringTokenizer;

public class boj_1197 {
    static class Node implements Comparable<Node>{
        public int to, cost;
        public Node(int to, int cost){
            this.to = to;
            this.cost = cost;
        }
        public int compareTo(Node o){
            return this.cost - o.cost;
        }
    }

    static int V, E;    // 정점 갯수, 간선 갯수
    static LinkedList<Node> list[];   // 정점 연결하는 연결리스트
    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(in.readLine());

        V = Integer.parseInt(st.nextToken());
        E = Integer.parseInt(st.nextToken());

        //연결리스트 초기화
        list = new LinkedList[V+1];
        for(int i=1; i<=V; i++)
            list[i] = new LinkedList<>();

        for(int i=0; i<E; i++){
            st = new StringTokenizer(in.readLine());
            // 각 정점별 간선, 가중치를 노드로 저장해 연결리스트 만들기
            int from = Integer.parseInt(st.nextToken());
            int to = Integer.parseInt(st.nextToken());
            int cost = Integer.parseInt(st.nextToken());
            list[from].add(new Node(to, cost));
            list[to].add(new Node(from, cost));
        }
        System.out.println(prim());
    }

    public static int prim(){
        // 가중치 누적합, mst에 포함된 정점 개수
        int result = 0, cnt = 0;

        // 간선 가중치가 작은 순대로 우선순위큐에 저장
        PriorityQueue<Node> pq = new PriorityQueue<>();
        boolean[] visited = new boolean[V+1];   // 정점이 mst에 속해있는지 표시
        int[] minEdge = new int[V+1];       // 정점별 최소값 저장
        Arrays.fill(minEdge, Integer.MAX_VALUE);    // 정점을 무한대값으로 초기화

        pq.offer(new Node(1, 0));   // 정점 1부터 출발
        minEdge[1] = 0;

        while(!pq.isEmpty()){
            Node minNode = pq.poll();   // 간선값 최소인 정점 poll
            if(visited[minNode.to]) continue;   //만약 이미 mst에 포함된 정점이면 continue

            visited[minNode.to] = true;
            result += minNode.cost;     // 정점 cost를 누적으로 더해주기
            if(++cnt == V)  break;      // mst에 포함된 정점 개수가 V면 탐색 종료

            // 현재 정점에 연결된 다른 정점들 탐색. pq에 넣기
            for(Node node : list[minNode.to]){
                if(!visited[node.to] && node.cost < minEdge[node.to]){
                    minEdge[node.to] = node.cost;
                    pq.offer(new Node(node.to, node.cost));
                }
            }
        }
        return result;
    }
}

Kruskal

간선을 하나씩 선택해서 MST를 찾는 알고리즘이다. 모든 간선을 가중치 기준으로 오름차순해서 정렬하고, 이 간선들을 순서대로 모든 정점이 연결될 때 까지 연결하는 것이다. 핵심은 Union-Find 알고리즘을 이용해서 구현하며, 이를 통해 사이클이 형성되지 않으면 정점을 연결할 수 있다.

크루스칼은 우선수위큐에서 간선의 가중치를 정렬하는 로직의 시간과 동일하다. 퀵 정렬의 시간 복잡도 = O(ElogE) 그러므로 간선이 적을 때는 크루스칼이 적합하다.

Union-Find

image 하나의 집합을 하나의 트리로 표현하는 방법이다. 자식 노드가 부모 노드를 가리키며 루트 노드가 대표자가 된다. 이를 통해 부모 노드가 같으면 사이클이 생겼다는 것을 판단할 수 있다.

Make-Set(x)
    p[x] = x;
Find-Set(x)
    IF x == p[x]: return x
    ELSE: return p[x] = Find-Set(p[x])


크루스칼 알고리즘

image

image

image

크루스칼 알고리즘

MST_KRUSKAL(G, w)
    FOR vertex v in G.V     // G.V: 그래프의 정점 집합
        Make_Set(v)         // G.E: 그래프의 간선 집합

    G.E에 포함된 간선들을 가중치 w에 의해 정렬

    FOR 가중치가 가장 낮은 간선 (u, v) in G.E 선택(n-1)
        IF Find_Set(u) != Find_Set(v)
            가중치 합
            Union(u, v);

백준 1197을 kruskal로 풀 경우

package boj;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.PriorityQueue;
import java.util.StringTokenizer;

public class boj_1197_kruskal {
    static class Node implements Comparable<Node>{
        public int from, to, cost;
        public Node(int from, int to, int cost){
            this.from = from;
            this.to = to;
            this.cost = cost;
        }
        public int compareTo(Node o){
            return this.cost - o.cost;
        }
    }
    // 부모 노드 찾기
    public static int find_set(int a){
        if(a == parent[a])  return a;
        else    return parent[a] = find_set(parent[a]);
    }

    //union
    public static void union(int a, int b){
        int aroot = find_set(a);
        int broot = find_set(b);
        parent[broot] = aroot;
    }

    static int V, E;    // 정점 갯수, 간선 갯수
    static PriorityQueue<Node> pq;
    static int[] parent;    // 부모 노드 저장
    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(in.readLine());

        V = Integer.parseInt(st.nextToken());
        E = Integer.parseInt(st.nextToken());

        // 간선 저장할 우선순위 큐
        pq = new PriorityQueue<Node>();
        for(int i=0; i<E; i++){
            st = new StringTokenizer(in.readLine());
            // 각 정점별 간선, 가중치를 노드로 저장해 연결리스트 만들기
            int from = Integer.parseInt(st.nextToken());
            int to = Integer.parseInt(st.nextToken());
            int cost = Integer.parseInt(st.nextToken());
            pq.offer(new Node(from, to, cost));
        }
        // parent 초기화
        parent = new int[V+1];
        for(int i=1; i<=V; i++)
            parent[i] = i;

        System.out.println(kruskal());
    }

    public static int kruskal(){
        int result = 0, cnt = 0;

        while(!pq.isEmpty()){
            Node node = pq.poll();
            if(find_set(node.from) != find_set(node.to)){
                cnt++;
                result += node.cost;
                union(node.from, node.to);
            }
            if(cnt == E-1)  break;
        }
        return result;
    }
}