(2)

[백준 1774번, 백준 4386번] 우주선, 별자리 만들기 - 최소 신장 트리(MST)

문제 난이도GOLD III   1774번 풀이간선의 가중치가 주어지지 않아 간선의 가중치를 구하는 로직만 추가해준다면 쉽게 문제를 해결할 수 있다. 이 문제에서 간선의 가중치는 2차원에 있는 신들의 위치 사이의 거리를 구하는 것으로, 아래 공식처럼 유클리드 거리 계산 방법을 이용하면 쉽게 가중치를 구할 수 있다.   위 공식을 코드로 나타낸다면 아래와 같다. private static double calCost(int x1, int x2, int y1, int y2) {    return Math.sqrt(Math.pow(x1 - y1, 2) + Math.pow(x2 - y2, 2));}       크루스칼(Kruskal) 알고리즘 (with. 백준 1197번 최소 스패닝 트리)크루스칼 알고리즘 (Kru..

크루스칼(Kruskal) 알고리즘 (with. 백준 1197번 최소 스패닝 트리)

크루스칼 알고리즘 (Kruskal Algorithm)크루스칼 알고리즘(Kruskal Algorithm)이란 최소 신장 트리(Minimum Spanning Tree)를 찾기 위해 사용되는 알고리즘이다. 신장 트리(Spanning Tree)그래프 내의 모든 정점을 포함하는 트리 최소 신장 트리(Minimum Spanning Tree)신장 트리 중에서 사용된 간선들의 가중치 합이 최소인 트리   최소 신장 트리를 찾기 위해 모든 간선을 가중치에 따라 오름차순으로 정렬하는 아이디어를 사용하여 효과적으로 MST를 구할 수 있게 된다.세부적인 구현 방법은 다음과 같다. 1. 모든 간선을 가중치에 따라 오름차순 정렬한다.2. 간선을 하나씩 선택하여 사이클이 생기지 않을 때 연결하여 모든 노드가 연결될 때 까지 반복한..

[백준 1774번, 백준 4386번] 우주선, 별자리 만들기 - 최소 신장 트리(MST)

P.S./백준 2024. 7. 19. 21:10
728x90
728x90

문제 난이도

GOLD III
 
 
 

1774번 풀이

간선의 가중치가 주어지지 않아 간선의 가중치를 구하는 로직만 추가해준다면 쉽게 문제를 해결할 수 있다.
 
이 문제에서 간선의 가중치는 2차원에 있는 신들의 위치 사이의 거리를 구하는 것으로, 아래 공식처럼 유클리드 거리 계산 방법을 이용하면 쉽게 가중치를 구할 수 있다.
 

 
 
위 공식을 코드로 나타낸다면 아래와 같다.
 

private static double calCost(int x1, int x2, int y1, int y2) {
    return Math.sqrt(Math.pow(x1 - y1, 2) + Math.pow(x2 - y2, 2));
}

 
 
 
 
 
 

 

크루스칼(Kruskal) 알고리즘 (with. 백준 1197번 최소 스패닝 트리)

크루스칼 알고리즘 (Kruskal Algorithm)크루스칼 알고리즘(Kruskal Algorithm)이란 최소 신장 트리(Minimum Spanning Tree)를 찾기 위해 사용되는 알고리즘이다. 신장 트리(Spanning Tree)그래프 내의 모든 정점을 포

mag1c.tistory.com

 
 
직전에 크루스칼 알고리즘에 대해 알아보았기 때문에 크루스칼 알고리즘으로 문제를 해결했다.
 
간선의 가중치를 위 공식으로 알아냈기 때문에, 간선을 기준으로 오름차순 정렬을 진행하여 Union-Find로 문제를 해결할 수 있다.
 
내가 문제를 풀이한 순서는 다음과 같다.
 

1. 입력 받기

입력을 Map형태로 받았다.
Key는 노드, Value는 해당 노드의 좌표를 받았다.

HashMap<Integer, int[]> map = new HashMap<>();

for (int i = 1; i <= N; i ++) {
    st = new StringTokenizer(br.readLine());
    int a = Integer.parseInt(st.nextToken());
    int b = Integer.parseInt(st.nextToken());
    map.put(i, new int[] {a, b});
}

 
 

2. Union-Find를 위한 각 노드의 부모 배열 생성 및 초기화

문제에서, 이미 연결된 각 노드들의 정보를 제공해 주었기에, union을 통해 부모 정보를 담고있는 배열을 업데이트 했다.

int[] parent = new int[N + 1];
//초기화
for (int i = 1; i <= N; i ++) {
    parent[i] = i;
}

//문제 조건 업데이트
for (int i = 0; i < M; i ++) {
    st = new StringTokenizer(br.readLine());
    int a = Integer.parseInt(st.nextToken());
    int b = Integer.parseInt(st.nextToken());
    union(parent, a, b);
}

 
 

3. 간선 가중치 계산

List<Node> lis = makeGraph(parent, N, map);

//신들의 그래프를 만든다.
// 이 때 find를 이용하여 연결되지 않은 신들의 가중치만을 계산한다.
// 이미 연결 되어있는 신들 간의 가중치는 계산할 필요가 없다.
private static List<Node> makeGraph(int[] parent, int N, HashMap<Integer, int[]> map) {
    List<Node> lis = new ArrayList<>();

    for (int i = 1; i <= N; i ++) {
        for (int j = 1; j <= N; j ++) {
            if (find(parent, i) != find(parent, j)) {
                double cost = calCost(map, i, j);
                lis.add(new Node(i, j, cost));
            }
        }
    }

    Collections.sort(lis, (o1, o2) -> Double.compare(o1.cost, o2.cost));

    return lis;
}


//가중치 계산
private static double calCost(HashMap<Integer, int[]> map, int x, int y) {
    int[] xLocation = map.get(x);
    int[] yLocation = map.get(y);

    return Math.sqrt(Math.pow(xLocation[0] - yLocation[0], 2) + Math.pow(xLocation[1] - yLocation[1], 2));
}

 
 
 

4. Union-Find를 이용해 문제 해결

마지막으로, 연결되지 않은 노드들의 가중치를 바탕으로 최소 신장 트리를 만들면서 문제에서 요구하는 가중치를 구한다.
요구조건에 맞게, 소숫점 두 번째 자리까지 출력한다.

List<Node> lis = makeGraph(parent, N, map);
double mst = 0;

for (Node n: lis) {
    int n1 = n.node1;
    int n2 = n.node2;

    if (find(parent, n1) != find(parent, n2)) {
        union(parent, n1, n2);
        mst += n.cost;
    }
}

System.out.printf("%.2f\n", mst);

 
 
 
 

전체 코드

public class Main {

    public static class Node {

        int node1;
        int node2;
        double cost;

        public Node(int n1, int n2, double c) {
            this.node1 = n1;
            this.node2 = n2;
            this.cost = c;
        }
        
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        int M = Integer.parseInt(st.nextToken());

        HashMap<Integer, int[]> map = new HashMap<>();


        for (int i = 1; i <= N; i ++) {
            st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            map.put(i, new int[] {a, b});
        }

        int[] parent = new int[N + 1];

        for (int i = 1; i <= N; i ++) {
            parent[i] = i;
        }

        for (int i = 0; i < M; i ++) {
            st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            union(parent, a, b);
        }

        List<Node> lis = makeGraph(parent, N, map);
        double mst = 0;

        for (Node n: lis) {
            int n1 = n.node1;
            int n2 = n.node2;

            if (find(parent, n1) != find(parent, n2)) {
                union(parent, n1, n2);
                mst += n.cost;
            }
        }

        System.out.printf("%.2f\n", mst);

    }

    private static List<Node> makeGraph(int[] parent, int N, HashMap<Integer, int[]> map) {
        List<Node> lis = new ArrayList<>();

        for (int i = 1; i <= N; i ++) {
            for (int j = 1; j <= N; j ++) {
                if (find(parent, i) != find(parent, j)) {
                    double cost = calCost(map, i, j);
                    lis.add(new Node(i, j, cost));
                }
            }
        }

        Collections.sort(lis, (o1, o2) -> Double.compare(o1.cost, o2.cost));

        return lis;
    }

    private static double calCost(HashMap<Integer, int[]> map, int x, int y) {
        int[] xLocation = map.get(x);
        int[] yLocation = map.get(y);

        return Math.sqrt(Math.pow(xLocation[0] - yLocation[0], 2) + Math.pow(xLocation[1] - yLocation[1], 2));
    }

    private static int find(int[] parent, int x) {
        if (parent[x] == x) {
            return x;
        }

        return parent[x] = find(parent, parent[x]);
    }

    private static void union(int[] parent, int a, int b) {
        a = find(parent, a);
        b = find(parent, b);

        if (a == b) return;
        if (a < b) {
            parent[b] = a;
        }
        else {
            parent[a] = b;
        }
    }
}

 

 

 

 

 

 

 

4386번 풀이

위 풀이에서, 미리 Union을 수행하는 부분만 제외하면 똑같은 문제이다.

대신에 이미 이차원 평면에서 각 노드의 좌표를 double로 받아서 계산만 해주면 된다.

자세한 풀이는 1774번과 거의 흡사하니 위 풀이를 참고한다면 금방 풀이가 가능하다.

 

public class Main {

    public static class Node {

        int node1;
        int node2;
        double cost;

        public Node(int n1, int n2, double c) {
            this.node1 = n1;
            this.node2 = n2;
            this.cost = c;
        }
        
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());              

        HashMap<Integer, double[]> map = new HashMap<>();
        StringTokenizer st;

        for (int i = 1; i <= N; i ++) {
            st = new StringTokenizer(br.readLine());
            double x = Double.parseDouble(st.nextToken());
            double y = Double.parseDouble(st.nextToken());
            map.put(i, new double[] {x, y});
        }

        int[] parent = new int[N + 1];

        for (int i = 1; i <= N; i ++) {
            parent[i] = i;
        }

        List<Node> lis = makeGraph(parent, N, map);
        double mst = 0;

        for (Node n: lis) {
            int n1 = n.node1;
            int n2 = n.node2;

            if (find(parent, n1) != find(parent, n2)) {
                union(parent, n1, n2);
                mst += n.cost;
            }
        }

        System.out.printf("%.2f\n", mst);

    }

    private static List<Node> makeGraph(int[] parent, int N, HashMap<Integer, double[]> map) {
        List<Node> lis = new ArrayList<>();

        for (int i = 1; i <= N; i ++) {
            for (int j = 1; j <= N; j ++) {
                if (find(parent, i) != find(parent, j)) {
                    double cost = calCost(map, i, j);
                    lis.add(new Node(i, j, cost));
                }
            }
        }

        Collections.sort(lis, (o1, o2) -> Double.compare(o1.cost, o2.cost));

        return lis;
    }

    private static double calCost(HashMap<Integer, double[]> map, int x, int y) {
        double[] xLocation = map.get(x);
        double[] yLocation = map.get(y);

        return Math.sqrt(Math.pow(xLocation[0] - yLocation[0], 2) + Math.pow(xLocation[1] - yLocation[1], 2));
    }

    private static int find(int[] parent, int x) {
        if (parent[x] == x) {
            return x;
        }

        return parent[x] = find(parent, parent[x]);
    }

    private static void union(int[] parent, int a, int b) {
        a = find(parent, a);
        b = find(parent, b);

        if (a == b) return;
        if (a < b) {
            parent[b] = a;
        }
        else {
            parent[a] = b;
        }
    }
}
728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

크루스칼(Kruskal) 알고리즘 (with. 백준 1197번 최소 스패닝 트리)

Tech/알고리즘 2024. 7. 16. 16:15
728x90
728x90

 

크루스칼 알고리즘 (Kruskal Algorithm)

크루스칼 알고리즘(Kruskal Algorithm)이란 최소 신장 트리(Minimum Spanning Tree)를 찾기 위해 사용되는 알고리즘이다.

 

신장 트리(Spanning Tree)
그래프 내의 모든 정점을 포함하는 트리

 

최소 신장 트리(Minimum Spanning Tree)
신장 트리 중에서 사용된 간선들의 가중치 합이 최소인 트리

 

 

 

최소 신장 트리를 찾기 위해 모든 간선을 가중치에 따라 오름차순으로 정렬하는 아이디어를 사용하여 효과적으로 MST를 구할 수 있게 된다.

세부적인 구현 방법은 다음과 같다.

 

1. 모든 간선을 가중치에 따라 오름차순 정렬한다.

2. 간선을 하나씩 선택하여 사이클이 생기지 않을 때 연결하여 모든 노드가 연결될 때 까지 반복한다.

 

여기서, 사이클의 생성 여부와 노드의 연결은 Union-Find를 통해 수행된다.

사이클의 생성 여부를 Find를 통해 확인하여 연결이 가능한 상태라면 Union을 수행하는 것이다.

 

 

 

예제를 통한 이해

이해를 돕기 위해 실제 P.S. 예제인 백준의 최소 스패닝 트리 문제로 차근차근 알아가보자.

 

 

 

 

입력 첫 줄에 정점(Vertex)과 간선(Edge)이 주어지고, 간선에 대한 정보들이 주어진다.

각 간선에 대한 정보에는 A와 B의 정점이 가중치가 C인 간선으로 연결되어 있다고 한다.

 

예제 데이터는 위키 백과의 크루스칼 알고리즘 글에서 발췌해서 사용했다.

 

출처: 위키백과 크루스칼 알고리즘

 

위 그래프의 간선들의 정보를 저장하면 다음과 같다. (편의상 A = 1, B = 2와 같이 표현했다.)

 



 

모든 간선의 정보를, 간선을 기준으로 오름차순 정렬하여 하나하나 연결하면 된다.

최소 가중치에 대한 보장은, 이미 오름차순 정렬을 통해 연결되는 간선이 최소 가중치라고 보장받을 수 있게 된다.

또한 위에서 말한 것 처럼 Union-Find를 사용하여 사이클의 여부를 파악하고 연결을 수행하면 되겠다.

 

하나하나 연결해보자.

 

 

 

이제 다음 상황인 2와 5의 연결에서 2의 부모 노드는 1이고 5의 부모 노드는 3이다.

부모 노드가 다름은 곧 연결이 되지 않았음을 의미하기 때문에 Union을 진행한다.

Union 과정에서 5번 노드의 부모 노드가 바뀌는 것이 아니라, 각 노드의 부모 노드인 1, 3번 노드의 연결을 내부적으로 수행하게 된다.

 

위 그림처럼 5번 노드의 부모 노드가 변경되는 것이 아닌, 3번 노드의 부모 노드가 변경된다.

 

 

 

이제 2번 노드와 3번 노드의 차례인데, 이미 같은 부모 노드를 가지고 있다.

위에서 언급한 사이클이라는 것이 바로 이런 경우를 말하는데, 같은 부모 노드를 가질 경우 사이클이 존재한다고 판단한다.

즉, 최소 신장 트리를 구해야 하기 때문에 굳이 추가로 연결하지 않는다.

 

 

 

 

5번과 6번 노드를 확인할 때도 마찬가지다.

5번 노드의 부모 노드를 따라가보면, 5 -> 3 -> 1번 노드가 나오고, 6번 노드 또한 1번 노드이다.

재귀적으로 부모 노드를 탐색하여 결국 5번 노드의 부모 노드는 1번 노드가 되고 부모 노드가 같기 때문에 Union은 진행하지 않는다.

 

 

 

 

 

이와 같은 방식으로, 계속해서 사이클 여부를 판단하여 연결을 진행해준다.

 

 

 

 

시간 복잡도

Union-Find 알고리즘의 시간 복잡도가 상수이기 때문에, 정렬을 수행하는 데 걸리는 시간이 곧 크루스칼 알고리즘의 시간복잡도가 된다.

일반적으로, 정점과 간선이 주어질 때 O(ElogV)의 시간복잡도를 가진다.

 

 

 

 

구현 코드(with Java, 백준 1197번)

위의 백준 1197번을 한 번 풀어보자.

위에서 언급했던 것 처럼, 구현 방법은 정렬 -> 사이클 파악 여부에 따른 연결로 진행된다.

 

 

입력받은 정보를 바탕으로 간선의 가중치를 기준으로 오름차 정렬한다.

int[][] graph = new int[E][3];

for (int i = 0; i < E; i++) {
    StringTokenizer st = new StringTokenizer(br.readLine());
    int v1 = Integer.parseInt(st.nextToken());
    int v2 = Integer.parseInt(st.nextToken());
    int edge = Integer.parseInt(st.nextToken());
    graph[i][0] = v1;
    graph[i][1] = v2;
    graph[i][2] = edge;
}

Arrays.sort(graph, (o1, o2) -> o1[2] - o2[2]);

 

 

 

사이클이 존재하지 않는 경우(부모 노드가 서로 다를 경우) 연결한다.

int mstCost = 0;

for (int i = 0; i < E; i ++)  {
    int v1 = graph[i][0];
    int v2 = graph[i][1];
    if (find(parent, v1) != find(parent, v2)) {
        union(parent, v1, v2);
        mstCost += graph[i][2];
    }
}

 

 

 

전체 코드는 아래와 같다.

public class Main {

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int V = Integer.parseInt(st.nextToken());
        int E = Integer.parseInt(st.nextToken());

        int[][] graph = new int[E][3];

        for (int i = 0; i < E; i++) {
            st = new StringTokenizer(br.readLine());
            int v1 = Integer.parseInt(st.nextToken());
            int v2 = Integer.parseInt(st.nextToken());
            int edge = Integer.parseInt(st.nextToken());
            graph[i][0] = v1;
            graph[i][1] = v2;
            graph[i][2] = edge;
        }
        
        int[] parent = new int[V + 1];

        for (int i = 1; i <= V; i ++) {
            parent[i] = i;
        }
        
        Arrays.sort(graph, (o1, o2) -> o1[2] - o2[2]);
        int mstCost = 0;

        for (int i = 0; i < E; i ++)  {
            int v1 = graph[i][0];
            int v2 = graph[i][1];
            if (find(parent, v1) != find(parent, v2)) {
                union(parent, v1, v2);
                mstCost += graph[i][2];
            }
        }

        bw.write(String.valueOf(mstCost));
        bw.flush();
        bw.close();
    }

    private static int find(int[] parent, int x) {
        if (parent[x] == x) {
            return x;
        }

        return parent[x] = find(parent, parent[x]);
    }

    private static void union(int[] parent, int x, int y) {
        x = find(parent, x);
        y = find(parent, y);

        if (x == y) return;
        
        if (x < y) {
            parent[y] = x;
        }
        else parent[x] = y;
    }
}

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

방명록