안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다. 

이번 포스팅은 백준 친구 네트워크 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/4195

 

4195번: 친구 네트워크

첫째 줄에 테스트 케이스의 개수가 주어진다. 각 테스트 케이스의 첫째 줄에는 친구 관계의 수 F가 주어지며, 이 값은 100,000을 넘지 않는다. 다음 F개의 줄에는 친구 관계가 생긴 순서대로 주어진

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*;
import java.util.HashMap;
import java.util.Map;

import static java.lang.Integer.parseInt;

public class Main {

    static int[] parent; // 부모 노드 설정
    static int[] cnt; // 부모에 있는 노드의 자식 노드 개수

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

        int t = parseInt(br.readLine());
        for (int i = 0; i < t; i++) {

            Map<String, Integer> map = new HashMap<>(); // 각 이름과 인덱스를 저장할 map
            int n = parseInt(br.readLine()); // 배열을 초기화할 개수

            // n개의 연결관계가 있으므로 n * 2가 최대
            parent = new int[n * 2];
            cnt = new int[n * 2];

            for (int j = 0; j < parent.length; j++) {
                parent[j] = j; // 부모 노드 자신으로 초기화
                cnt[j] = 1; // 개수는 모두 1로 초기화
            }

            int idx = 0; // 인덱스 0 설정
            for (int j = 0; j < n; j++) {
                String[] str = br.readLine().split(" ");

                if (!map.containsKey(str[0])) map.put(str[0], idx++);
                if (!map.containsKey(str[1])) map.put(str[1], idx++);

                union(map.get(str[0]), map.get(str[1])); // 두 친구 연결하기
                sb.append(cnt[find(map.get(str[1]))]).append("\n"); // 두번째 인원의 부모를 찾아서 네트워크 연결된 수 찾기
            }
        }

        System.out.print(sb);
    }

    static int find(int child) { // find
        if (parent[child] == child) return child;
        return parent[child] = find(parent[child]); // 경로 압축
    }

    static void union(int c1, int c2) { // 합치기
        int p1 = find(c1); // c1의 부모 노드 
        int p2 = find(c2); // c2의 부모 노드

        if (p1 == p2) return;
        parent[p2] = p1;
        cnt[p1] += cnt[p2]; // p1의 자식 노드 수 p2의 자식 노드 수 합치기
        cnt[p2] = 0; // p2 인원 초기화
    }
}

 

2. 풀이 중점 사항

 

친구 네트워크는, 서로 다른 집합의 네트워크가 분산되어 있을 때, 하나로 연결하는 작업을 수행해야 합니다.

이렇게 서로 다른 집합을 서로 연결해야 하는 문제가 주어졌을 때, 적용할 수 있는 알고리즘이 유니온 파인드입니다.

 

static int find(int child) { // find
    if (parent[child] == child) return child;
    return parent[child] = find(parent[child]); // 경로 압축
}

static void union(int c1, int c2) { // 합치기
    int p1 = find(c1); // c1의 부모 노드 
    int p2 = find(c2); // c2의 부모 노드

    if (p1 == p2) return;
    parent[p2] = p1;
}

 

부모 노드에 속한 자식 노드 개수 구하기

 

p1, p2의 노드를 유니온 파인드 진행할 때, p1의 부모 노드의 자식 노드의 개수와 p2의 부모 노드의 자식 노드 개수를 모두 더해야 합니다. 이를 구현하기 위해 cnt 배열을 선언하였습니다.

 

자식 노드의 개수를 늘리는 로직은 union을 수행할 때 적용할 수 있습니다.

만약 두 부모 노드가 다르다면, 부모 노드 중 합쳐서 집합이 더 커진 대상을 기준으로 합쳐진(합쳐져서 없어진 집합)의 자식 노드 수를 더해줍니다. 

 

parent[p2] = p1;
cnt[p1] += cnt[p2]; // p1의 자식 노드 수 p2의 자식 노드 수 합치기
cnt[p2] = 0; // p2 인원 초기화

 

이상으로 자바 친구 네트워크 풀이를 마치도록 하겠습니다. 감사합니다.

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 문제는 백준 플로이드-와샬(워샬) 문제 플로이드(11404) 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/11404

 

11404번: 플로이드

첫째 줄에 도시의 개수 n이 주어지고 둘째 줄에는 버스의 개수 m이 주어진다. 그리고 셋째 줄부터 m+2줄까지 다음과 같은 버스의 정보가 주어진다. 먼저 처음에는 그 버스의 출발 도시의 번호가

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*;
import java.util.*;
import static java.lang.Integer.parseInt;

public class Main {
    static final int INF = 100_000_001; // 100 * 100만 범위 이므로, +1 큰 것은 넘을 수 없음
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = parseInt(br.readLine());
        int m = parseInt(br.readLine());     
        int[][] graph = new int[n + 1][n + 1]; // dp 그래프를 위한 배열 초기화

        for (int row = 1; row <= n; row++) {
            for (int col = 1; col <= n; col++) {
                if (row == col) continue; // 행과 열이 같다면, 노드에서 노드로 이동하므로 0
                graph[row][col] = INF; // 출발지와 도착지가 다른 노드는 INF로 초기화
            }
        }

        for (int i = 0; i < m; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int e = parseInt(st.nextToken()); // 출발지 노드
            int v = parseInt(st.nextToken()); // 도착지 노드
            int w = parseInt(st.nextToken()); // 비용
            graph[e][v] = Math.min(graph[e][v], w); // 간선이 하나가 아닐 수 있다는 정보가 있으므로
        }

        
        for (int way = 1; way <= n; way++) { // 경유하려는 노드
            for (int row = 1; row <= n; row++) { // 출발지 노드
                if (row == way) continue; // 만약 출발지 노드와 경유하려는 노드가 같다면 패스

                for (int col = 1; col <= n; col++) { // 도착지 노드
                    // 출발지 노드와 도착지 노드가 같다면 패스
                    // 도착지 노드와 경유하려는 노드가 같다면 패스
                    if (row == col || col == way) continue;
                    graph[row][col] = Math.min(graph[row][way] + graph[way][col], graph[row][col]); // 출발 -> 경유 + 경유 -> 출발 , 출발 -> 도착 비교 
                }
            }
        }

        StringBuilder sb = new StringBuilder();
        for (int row = 1; row <= n; row++) {
            for (int col = 1; col <= n; col++) {
                if (graph[row][col] == INF) sb.append(0).append(" ");
                else sb.append(graph[row][col]).append(" ");
            }
            sb.append("\n");
        }
        
        System.out.print(sb);
    }
}

 

2. 플로이드-와샬(워샬) 알고리즘 

 

플로이드-와샬 알고리즘은 그래프의 모든 노드쌍에 대한 최단 거리를 구하는 알고리즘입니다. 다익스트라 알고리즘은 한 노드에서 다른 모든 노드로의 최단 거리를 구하는 알고리즘이지만, 플로이드 와샬은 모든 노드 쌍 사이의 최단 거리를 구할 수 있습니다.

 

여기서 활용되는 원리가 DP입니다.

출발지 -> 도착지 노드로 가는 거리보다, 경유지를 들려서 가는 거리가 더 짧을수도 있습니다.

이러한 경우를 효율적으로 계산하기 위해 특정 노드에서 특정 노드로 가는 거리를 dp에 저장하고,

만약 경유지로 해당 거리를 미리 저장해놓았다면, dp 테이블에서 이를 활용하여 경유지 거리를 계산하는 것입니다.

 

dp를 활용하여, 1 -> 2까지의 거리를 저장해 놓았다면 1 -> 2 + 2 -> 3의 거리와 1 -> 3까지의 거리를 비교하여 최소 거리를 찾을 수 있습니다.

 

for (int way = 1; way <= n; way++) { // 경유하려는 노드
    for (int row = 1; row <= n; row++) { // 출발지 노드
        if (row == way) continue; // 만약 출발지 노드와 경유하려는 노드가 같다면 패스

        for (int col = 1; col <= n; col++) { // 도착지 노드
            // 출발지 노드와 도착지 노드가 같다면 패스
            // 도착지 노드와 경유하려는 노드가 같다면 패스
            if (row == col || col == way) continue;
            graph[row][col] = Math.min(graph[row][way] + graph[way][col], graph[row][col]); // 출발 -> 경유 + 경유 -> 출발 , 출발 -> 도착 비교 
        }
    }
}

 

n개의 노드를 경유하고, 각 출발지 노드 n개, 도착지 노드 n를 설정해야하므로 시간복잡도는 O(n^3)을 가지게 됩니다.

 

이상으로 백준 플로이드-와샬 문제 플로이드 풀이를 마치도록 하겠습니다.! 감사합니다.!!!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 벨만포드 문제 - 타임머신 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/11657

 

11657번: 타임머신

첫째 줄에 도시의 개수 N (1 ≤ N ≤ 500), 버스 노선의 개수 M (1 ≤ M ≤ 6,000)이 주어진다. 둘째 줄부터 M개의 줄에는 버스 노선의 정보 A, B, C (1 ≤ A, B ≤ N, -10,000 ≤ C ≤ 10,000)가 주어진다. 

www.acmicpc.net

 

1. 풀이 소스

 

import java.util.*;
import java.io.*;
import static java.lang.Integer.parseInt;

public class Main {
    static ArrayList<ArrayList<Edge>> graph = new ArrayList<>();
    static final int INF = Integer.MAX_VALUE;
    static long[] dist;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        int n = parseInt(st.nextToken()); // 정점 개수
        int m = parseInt(st.nextToken()); // 간선 개수
        for (int i = 0; i <= n; i++) graph.add(new ArrayList<>());
        for (int i = 0; i < m; i++) {
            st = new StringTokenizer(br.readLine());
            int e = parseInt(st.nextToken());
            int v = parseInt(st.nextToken());
            int w = parseInt(st.nextToken());
            
            graph.get(e).add(new Edge(v, w)); // 그래프에 엣지 추가하기
        }
        
        dist = new long[n + 1]; // 정점 개수로 dist 초기화 노드 1번에서 각 노드 출발
        Arrays.fill(dist, INF);
        dist[1] = 0; // 시작노드인 1번은 = 0

        for (int k = 0; k < n - 1; k++) { // 벨만 포드 알고리즘은 k - 1번 반복
            for (int i = 1; i <= n; i++) { // 정점 개수
                ArrayList<Edge> edges = graph.get(i);

                for (Edge edge : edges) {
                    if (dist[i] != INF && dist[edge.v] > dist[i] + edge.w) {
                        dist[edge.v] = dist[i] + edge.w; // 출발 정점에서 도착 정점까지의 거리가, 이전의 값보다 작다면 업데이트
                    }
                }
            }
        }

        StringBuilder sb = new StringBuilder();

        for (int i = 1; i <= n; i++) { // 음수 사이클이 존재하는지 판단
            ArrayList<Edge> edges = graph.get(i);

            for (Edge edge : edges) {
                if (dist[i] != INF && dist[edge.v] > dist[i] + edge.w) { // 만약 음수 사이클로 인해 이전 값이 줄어든다면 무한 사이클 가능
                    sb.append(-1).append("\n");
                    System.out.print(sb);
                    return;
                }
            }
        }

        for (int i = 2; i <= n; i++) {
            if (dist[i] == INF) sb.append(-1).append("\n"); // 갈 수 없는 곳이라면
            else sb.append(dist[i]).append("\n");
        }

        System.out.print(sb);
    }
    
    static class Edge {
        int v; // 도착 정점
        int w; // 가중치
        
        Edge (int v, int w) {
            this.v = v;
            this.w = w;
        }
    }
}

 

2. 벨만포드 알고리즘

 

벨만포드 알고리즘은 그래프 최단 경로를 구하는 알고리즘으로, 하나의 정점에서 출발하는 최단 거리를 구하는 문제입니다. 

음수 가중치를 허용하는 것이 특징입니다.

 

벨만 포드 알고리즘은 다음의 절차로 이루어집니다.

  • 간선 m개를 하나씩 모두 탐색
  • dist[도착 정점] = min(dist[도착 정점], dist[출발지 정점] + cost(도착지 정점으로 가는 간선의 가중치))
  • 노드 개수의 n - 1개 만큼 이를 반복하여 특정 정점에서 각 노드로의 이동거리를 업데이트 진행

 

이를 그림의 예시로 설명하면 다음과 같습니다.

 

 

해당 그래프와 간선은 다음과 같이 주어졌다고 가정하겠습니다.

 

초기 dist배열은 1번 노드에서, 각 노드로의 최단 거리를 담고 있습니다. 1번 노드에서 1번 노드의 이동은 0으로 초기화하고, 나머지 노드로의 이동은 아직 간선으로 이동하지 않았으므로 무한대로 초기화합니다.

 

1번 노드에서 3번 노드로 연결된 간선을 통해 dist가 업데이트될 수 있습니다. 이때 조건은 다음과 같습니다.

 

if (dist[i] != INF && dist[edge.v] > dist[i] + edge.w) {
    dist[edge.v] = dist[i] + edge.w; // 출발 정점에서 도착 정점까지의 거리가, 이전의 값보다 작다면 업데이트
}

 

i는 이동을 시작하는 곳의 노드 번호이고, edge.v는 도착하려는 곳의 정점입니다.

dist[i], 즉 1번 노드의 dist[1] = 0 이고, 이동하려는 3번 노드의 거리는 무한대인데, dist[i] + edge.w가 1이므로 3번 노드의 거리를 업데이트합니다.

수식: dist[edge.v] > dist[i] + edge.w          =>           dist[3] > dist[1] + 1                  =>        ∞ > 1      =>    dist[3] = 1

 

다음은 2번 노드에서 4번 노드로 이동하는 간선입니다.

이때, dist[2] = ∞입니다. 따라서, 해당 간선은 dist[i] != INF라는 조건에 위배되어 실행되지 않고 넘어갑니다.

 

 

다음은 3번 노드에서 2번 노드로 이동하는 간선의 차례입니다.

dist[3] = 1이므로 dist[3] != INF를 만족합니다.

    dist[2] > dist[3] + 1 

 = ∞ > 2

=> dist[2] = 2를 만족하게 됩니다.

 

이렇게 종료된다면, 2 -> 4로 이동하는 것은 여전히 무한대로 남아있습니다.

앞 서, n - 1번을 반복하는 이유가 이러한 반례가 존재하기 때문입니다.

따라서, n - 1번 반복을 하게 된다면, 2번 노드가 현재 1로 업데이트 되어 있으므로 4번 노드까지의 거리를 업데이트할 수 있습니다.

 

 

3. 벨만포드 알고리즘 음수 간선

 

앞서 양수로 움직이는 벨만포드 알고리즘을 확인할 수 있었습니다. 벨만포드 알고리즘은 음수 간선을 적용할 수 있다는 점에서 다익스트라 알고리즘과 비교할 수 있습니다.

 

음수 간선이 적용된다면, 벨만포드 알고리즘은 어떻게 처리가 될까요?

 

이번 예시는

1 -> 2 : 1

2 -> 3 : 1

3 -> 1 : -1

로 처리되는 그래프를 예시로 확인해 보겠습니다.

 

이 그래프는 1 -> 2 -> 3 -> 1로 이어지는 사이클을 형성하고 있습니다.

벨만포드 알고리즘으로 각 간선을 적용하여 n - 1번 반복하며 값을 업데이트하면 다음과 같습니다.

1 -> 2 이동 거리 업데이트 

 

2 -> 3 이동 거리 업데이트

 

 

3 -> 1 거리 업데이트.

 

 

이렇게까지 종료를 하면, 올바른 답을 구하지 못할 수 있습니다.

이 예시에서는 이렇게 종료하더라도, 문제가 발생하지 않습니다.

하지만, 만약 음수 사이클이 존재하여 값이 무한대로 감소하게 된다면, 최단 거리를 구할 수 없게 됩니다.

 

4. 벨만포드로 음수 사이클 판단하기

 

예시를 조금 수정하여, 음수 사이클이 적용되는 예시를 만들어 보았습니다.

 

간선은

1 -> 2 : 1

2 -> 3 : 1

3 -> 1: -10으로 구성됩니다.

 

마찬가지로 각 간선으로 거리를 구하면 다음의 절차를 따릅니다.

 

 

1 -> 2 거리 업데이트 1

 

 

2 -> 3 거리 업데이트 2

 

3 -> 1 거리 업데이트 -8 

 

여기서 벨만포드 알고리즘은, 간선을 업데이트하는 방식을 한 번 더 적용하여, 해당 간선이 음수 사이클을 형성하고 있는지 판단할 수 있습니다.

지금 1 -> 2 -> 3 -> 1까지 거치는 최종 거리는 -8입니다.

만약 한 번 더 이동을 하게 된다면 1- > 2 -> 3 -> 1 -> 2 -> 3 -> 1 는 -16이 되게 됩니다.

즉, 무한번 사이클을 회전하게 된다면 그 값이 무한정 감소하게 되는 것입니다.

 

따라서 벨만포드 알고리즘은 다음의 절차로 정리할 수 있습니다

 

  • 모든 노드의 거리를 무한대로 설정하고 시작 노드의 거리만 0으로 설정합니다.
  • 모든 간선에 대해, 만약 시작 노드에서 해당 간선의 시작점까지의 현재 거리와 간선의 가중치의 합이 간선의 끝점까지의 현재 거리보다 작다면, 간선의 끝점까지의 거리를 업데이트합니다. 이 단계를 노드 수 - 1번 반복합니다.
  • 모든 간선을 다시 한 번 검사하여, 만약 위의 업데이트를 계속할 수 있다면 이는 음의 사이클이 존재함을 의미합니다.

 

5. 코드로 정리하기

 

for (int i = 1; i <= n; i++) { // 음수 사이클이 존재하는지 판단
    ArrayList<Edge> edges = graph.get(i);

    for (Edge edge : edges) {
        if (dist[i] != INF && dist[edge.v] > dist[i] + edge.w) { // 만약 음수 사이클로 인해 이전 값이 줄어든다면 무한 사이클 가능
            sb.append(-1).append("\n");
            System.out.print(sb);
            return;
        }
    }
}
  •  

앞 서, 음의 사이클이 존재하는지 여부는 이처럼 간선을 한 번더 탐색하여, 값이 업데이트되는 경우가 존재하는지 판단합니다.

dist[edge.v] > dist[i] + edge.w 가 실행된다는 것은, 이전에 업데이트한 값보다 더 작은 값이 생성된다는 의미이므로, 음의 사이클이 발생했다고 판단할 수 있습니다.

 

이상으로 벨만포드 알고리즘으로 해결할 수 있는 타임머신 문제 풀이를 마치도록 하겠습니다. 감사합니다!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 LCS 문제 풀이를 진행하고자 합니다.

문제 출처: https://www.acmicpc.net/problem/9251 

 

9251번: LCS

LCS(Longest Common Subsequence, 최장 공통 부분 수열)문제는 두 수열이 주어졌을 때, 모두의 부분 수열이 되는 수열 중 가장 긴 것을 찾는 문제이다. 예를 들어, ACAYKP와 CAPCAK의 LCS는 ACAK가 된다.

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String str1 = br.readLine();
        String str2 = br.readLine();
        
        int[][] dp = new int[str1.length() + 1][str2.length() + 1];
        
        for (int i = 1; i <= str1.length(); i++) {
            for (int j = 1; j <= str2.length(); j++) {
                if (str1.charAt(i - 1) == str2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1] + 1;
                else dp[i][j] = Math.max(dp[i- 1][j], dp[i][j - 1]);
            }
        }
        System.out.println(dp[str1.length()][str2.length()]);
    }
}

 

 

2. LCS

 

LCS는 Longest Common Subsequence로 두 문자열에서 가장 긴 공통부분 문자열을 찾는 문제를 의미합니다. 부분 문자열은 ABC와 ABC 두 문자열이 동일해야만 하는 것이 아니라, ABDC와 ABC 문자처럼 연속되지는 않지만 하나의 부분 문자열로 ABC를 뽑아낼 수 있는 문자열을 의미합니다.

 

해당 풀이는 다른 소스와 달리 주석을 설정하지 않았는데, 공식처럼 사용되는 부분이 있기 때문입니다. 이 부분을 그림으로 설명하도록 하겠습니다.

 

CAPCAK 문자열과 ACAYKP 두 문자열의 LCS를 구하면 다음과 같이 2차원 배열을 설정할 수 있습니다.

1행에 있는 문자 C와 각 열에 있는 문자 ACAYKP를 하나씩 검사하며, 같은 문자가 있는지 파악합니다.

1행의 C와 2열의 C가 같기 때문에 +1을 진행합니다.

 

1행의 C와 3열의 A는 같지 않습니다. 하지만, 최장 공통 부분 문자열이란, 공통의 긴 부분 문자열 쉽게 말해, 최대 몇 개까지 공통된 문자로 수열을 만들 수 있는가? 를 의미합니다.

따라서, 비록 C와 A는 같지 않지만, 이전 2열에서 C와 C가 동일하기 때문에 1을 그대로 이어서 가져오게 됩니다.

 

 

2행의 A는 2열의 A와 동일합니다. 이 경우는 +1 을 진행해야 하지만, 이전 행과 이전 열의 값에서 +1을 해야 하는 것을 그림에서 파악할 수 있습니다. 왜냐하면, 이전 행과 열을 파악한 결과 같아서 + 1을 했는데, 그 다음행과 그다음열이 또 같기 때문에 이전에 계산한 결과를 그대로 활용해야 하기 때문입니다. 따라서, 하나의 공식을 도출할 수 있습니다.

 

str1.charAt(i) == str2.charAt(j) :

          dp[i][j] = d[i - 1][j - 1] + 1

 

 

 

4행의 C와 6열의 P는 같지 않습니다. 그렇다면 그대로 유지를 해야 합니다. 이때 두 가지를 선택할 수 있습니다.

CAPC / ACAYK, CAPC / ACAYKP의 부분 문자열

먼저 전자는 CA로 2개가 동일하고, 후자는 CAP 3개가 동일합니다.

LCS는 가장 긴 공통 부분 문자열을 구해야 하므로, CAP 동일한 문자열 3개를 선택하게 됩니다.

 

여기서 하나의 공식을 도출할 수 있습니다.

 

str1.charAt(i) != str2.charAt(j) :

dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);

 

즉, 행과 열 중 1이 더 작은 값을 가지는 값에서 가장 큰 값을 가지는 것을 그대로 유지하는 것입니다.

이러한 원리로 모든 문자를 다 채우게 되면 다음과 같습니다.

 

 

for (int i = 1; i <= str1.length(); i++) {
    for (int j = 1; j <= str2.length(); j++) {
        if (str1.charAt(i - 1) == str2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1] + 1;
        else dp[i][j] = Math.max(dp[i- 1][j], dp[i][j - 1]);
    }
}
System.out.println(dp[str1.length()][str2.length()]);

 

이를 토대로 dp 마지막에 위치한 값을 각 dp 테이블에서 최대값을 따르게 되는 것을 확인할 수 있습니다.

 

이상으로 LCS 문제 풀이를 마치도록 하겠습니다. 감사합니다.!!!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 이분탐색 문제 랜선 자르기 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/1654

 

1654번: 랜선 자르기

첫째 줄에는 오영식이 이미 가지고 있는 랜선의 개수 K, 그리고 필요한 랜선의 개수 N이 입력된다. K는 1이상 10,000이하의 정수이고, N은 1이상 1,000,000이하의 정수이다. 그리고 항상 K ≦ N 이다. 그

www.acmicpc.net

 

1. 풀이 소스

 

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

import static java.lang.Integer.parseInt;

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

        int k = parseInt(st.nextToken());
        int n = parseInt(st.nextToken());

        int[] arr = new int[k]; // k개의 값을 저장할 배열
        long max = 0; // 최대값 저장 (가장 긴 랜선을 줄여 나가야 가장 큰 길이를 구할 수 있음)

        for (int i = 0; i < k; i++) {
            arr[i] = parseInt(br.readLine());
            if (max < arr[i]) max = arr[i]; // 최대값 업데이트
        }

        max++; //
        long min = 0;
        long mid = 0;

        while (min < max) { // 이분탐색으로 min < max인 조건으로 루프
            mid = (max + min) / 2; // 중간 값 설정하기

            long count = 0; // 카운트 개수 0 초기화

            for (int j : arr) {
                count += (j / mid); // count를 업데이트
                if (count >= n) break; // 개수가 n 보다 크면 종료
            }

            if (count < n) max = mid; // count
            else min = mid + 1; // upper bound로 min 값 증가
        }

        System.out.println(min - 1);
    }
}

 

2. 풀이 중점 사항

 

이번 문제는 이분 탐색의 upper bound를 활용하는 것이 중점 사항이었습니다.

도중에 47%에서 메모리 초과가 발생하여, 하단의 블로그를 참고하였습니다.

 

upper bound 방법은 이분 탐색의 한 종류로, 주어진 값보다 처음으로 큰 원소를 찾는 알고리즘입니다. 이분탐색의 차이점은 이분 탐색은 정확하게 특정 값을 찾는다면, upper bound는 이름처럼 가장 근접하게 큰 원소를 찾는 것이라고 설명할 수 있습니다.

예를 들어, min = 199, max = 201이 있다면, mid = (min + max) / 2 = 200이 됩니다. 

count < n이 성립하지 않으므로 min을 mid + 1 추가합니다. 이 과정에서, min < max 조건이 위배되게 되므로,

min = 201 상태에서 종료됩니다. 

upper bound는 앞 서 정의한 것처럼 가장 근접하게 큰 원소를 찾는 것이므로, 우리의 목표인 200보다 1 큰 값을 찾게 됩니다.

따라서, min - 1로 문제를 해결할 수 있었습니다.

 

이상으로 백준 랜선 자르기 풀이를 마치도록 하겠습니다. 감사합니다!!! 

 

 

참고 블로그: https://st-lab.tistory.com/269

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 누적합 문제 - 인간 컴퓨터 상호작용 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/16139

 

16139번: 인간-컴퓨터 상호작용

첫 줄에 문자열 $S$가 주어진다. 문자열의 길이는 $200,000$자 이하이며 알파벳 소문자로만 구성되었다. 두 번째 줄에는 질문의 수 $q$가 주어지며, 문제의 수는 $1\leq q\leq 200,000$을 만족한다. 세 번째

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*;
import java.util.*;
import static java.lang.Integer.parseInt;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        List<Question> qs = new ArrayList<>(); // question을 저장할 리스트
        
        String str = br.readLine();
        int n = str.length();
        int q = parseInt(br.readLine());
        for (int i = 0; i < q; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            qs.add(new Question(st.nextToken().charAt(0), parseInt(st.nextToken()), parseInt(st.nextToken())));
        }
        
        int[][] dp = new int[n + 1][26]; // dp 2차원 배열 (누적합)

        for (int i = 1; i <= n; i++) { // 인덱스 에러를 방지하기 위해 1부터 시작
            int num = str.charAt(i - 1) - 'a'; // char - 'a'로 정수로 변환
            for (int j = 0; j < 26; j++) {
                if (j == num) dp[i][num] = dp[i - 1][num] + 1; // 만약 해당 문자에 해당하는 정수라면 +1
                else dp[i][j] = dp[i - 1][j]; // 아니라면 그대로
            }
        }

        StringBuilder sb = new StringBuilder();
        for (Question question : qs) {
            int num = question.c - 'a';
            sb.append(dp[question.e][num] - dp[question.s][num]).append("\n"); //양 끝을 포함해야하므로 e는 1을 더해주고, s는 그대로
        }
        
        System.out.print(sb);
    }
    
    static class Question {
        char c;
        int s;
        int e;
        
        Question (char c, int s, int e) {
            this.c = c;
            this.s = s;
            this.e = e + 1; // 끝점 포함 
        }
    }
}

 

2. 풀이 중점 사항

 

문제 해결 방법은 문자열 길이 n를 탐색하며 26개의 알파벳을 누적합으로 업데이트하는 방법입니다.

시간 복잡도는 O(n * 26 + question.size()) 으로 계산할 수 있습니다. 시간제한이 1초이므로 n, quesion.size()가 최대 20만이므로 약 O(540만)입니다. 1초에 약 1억 번 연산이 가능하므로 문제를 해결할 수 있을 것이라고 판단하였습니다.

 

for (int i = 1; i <= n; i++) { // 인덱스 에러를 방지하기 위해 1부터 시작
    int num = str.charAt(i - 1) - 'a'; // char - 'a'로 정수로 변환
    for (int j = 0; j < 26; j++) {
        if (j == num) dp[i][num] = dp[i - 1][num] + 1; // 만약 해당 문자에 해당하는 정수라면 +1
        else dp[i][j] = dp[i - 1][j]; // 아니라면 그대로
    }
}

 

26개의 문자를 배열에 저장하기 위해 char - 'a'를 하여 0 ~ 26의 정수형을 만들었습니다.

이후, num의 값을 가지는 2차원 배열은 +1을 하고 나머지는 그대로 유지하도록 설정하였습니다.

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 백트래킹 문제 스도쿠 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/2580

 

2580번: 스도쿠

스도쿠는 18세기 스위스 수학자가 만든 '라틴 사각형'이랑 퍼즐에서 유래한 것으로 현재 많은 인기를 누리고 있다. 이 게임은 아래 그림과 같이 가로, 세로 각각 9개씩 총 81개의 작은 칸으로 이루

www.acmicpc.net

 

1. 풀이 소스

 

import java.util.*;
import java.io.*;
import static java.lang.Integer.parseInt;

public class Main {
    static boolean sudokuBreak; // 스도쿠를 찾으면 종료
    static int[][] map; // 값을 저장할 맵
    static boolean[] check = new boolean[10]; // row, col, block에서 값이 없는 숫자를 확인
    static List<Point> zeros = new ArrayList<>(); // zero가 저장된 위치 저장
    static Map<Integer, Set<Integer>> hashMap = new HashMap<>(); // hashMap에 스도쿠 row, col, block에 숫자 저장

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        
        map = new int[10][10]; // 맵 초기화
        zeros = new ArrayList<>(); // zero 초기화

        for (int row = 1; row <= 9; row++) {
            hashMap.put(row, new HashSet<>());
            hashMap.put(row * -1, new HashSet<>()); // col은 겹치지 않도록 음수로 저장
            hashMap.put(row + 100, new HashSet<>()); // block은 겹치지 않도록 + 100
        }

        for (int row = 1; row <= 9; row++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            for (int col = 1; col <= 9; col++) {
                int num = parseInt(st.nextToken());
                map[row][col] = num;
                if (num == 0) zeros.add(new Point(row, col)); // 값이 zero라면 zeros에 추가
            }
        }

        for (int row = 1; row <= 9; row++) {
            for (int col = 1; col <= 9; col++) {
                int num = map[row][col];
                if (num != 0) { // 값이 0이 아니라면, rowSet, colSet, blockSet에 추가
                    Set<Integer> rowSet = hashMap.get(row);
                    Set<Integer> colSet = hashMap.get(col * -1);

                    rowSet.add(num);
                    colSet.add(num);

                    Set<Integer> blockSet = getBlockSet(row, col);
                    blockSet.add(num);
                }
            }
        }

        for (Point p : zeros) setPossible(p); // zero 위치에서 가능한 숫자 저장

        StringBuilder sb = new StringBuilder();
        if (zeros.size() == 0) { // 만약 zeros가 0이라면 그대로 출력
            for (int i = 1; i <= 9; i++) {
                for (int j = 1; j <= 9; j++)
                    sb.append(map[i][j]).append(" ");
                sb.append("\n");
            }
            System.out.print(sb);
            return;
        }

        dfs(0); // 재귀

        for (int i = 1; i <= 9; i++) {
            for (int j = 1; j <= 9; j++)
                sb.append(map[i][j]).append(" ");
            sb.append("\n");
        }
        System.out.print(sb);
    }

    static void dfs(int count) {
        // 검사영역, row, col, 3 by 3
        if (count == zeros.size()) { // 만약 모두 만족했다면 종료
            sudokuBreak = true;
            return;
        }

        Point p = zeros.get(count); // 포인트 가져오기
        for (Integer i : p.possible) { // p 위치에서 가능한 숫자

            if (!isValid(p.row, p.col, i))  continue; // 불가능한 경우라면 패스

            map[p.row][p.col] = i; // 값 설정
            Set<Integer> rowSet = hashMap.get(p.row);
            rowSet.add(i); // rowSet에 추가

            Set<Integer> colSet = hashMap.get(p.col * -1);
            colSet.add(i); // colSet에 추가

            Set<Integer> blockSet = getBlockSet(p.row, p.col);
            blockSet.add(i); // 현재 row, col이 속한 block에 추가

            dfs(count + 1);

            if (sudokuBreak) return;
            map[p.row][p.col] = 0; // 백트래킹
            rowSet.remove(i);
            colSet.remove(i);
            blockSet.remove(i);
        }
        
    }
        
    static boolean isValid(int row, int col, int num) { // value가 있다면 종료
        Set<Integer> rowSet = hashMap.get(row);
        if (rowSet.contains(num)) return false;

        Set<Integer> colSet = hashMap.get(col * -1);
        if (colSet.contains(num)) return false;

        Set<Integer> blockSet = getBlockSet(row, col);
        return !blockSet.contains(num);
    }

    static Set<Integer> getBlockSet(int row, int col) { //block 설정하기
        Set<Integer> blockSet;
        if (row <= 3 && col <= 3) blockSet = hashMap.get(101);
        else if (row <= 6 && col <= 3) blockSet = hashMap.get(102);
        else if (col <= 3) blockSet = hashMap.get(103);
        else if (row <= 3 && col <= 6) blockSet = hashMap.get(104);
        else if (row <= 6 && col <= 6) blockSet = hashMap.get(105);
        else if (col <= 6) blockSet = hashMap.get(106);
        else if (row <= 3) blockSet = hashMap.get(107);
        else if (row <= 6) blockSet = hashMap.get(108);
        else blockSet = hashMap.get(109);
        return blockSet;
    }


    static void setPossible(Point p) { // 가능한 숫자 찾기
        int blockRow, blockCol;
        Set<Integer> possibles = new HashSet<>();
        
        if (p.row <= 3) blockRow = 1;
        else if (p.row <= 6) blockRow = 4;
        else blockRow = 7;
        
        if (p.col <= 3) blockCol = 1;
        else if (p.col <= 6) blockCol = 4;
        else blockCol = 7;

        Arrays.fill(check, false);
        for (int i = blockRow; i < blockRow + 3; i++) { // block에 없는 숫자 찾기
            for (int j = blockCol; j < blockCol + 3; j++) check[map[i][j]] = true;
        }        
        for (int i = 1; i <= 9; i++) if (!check[i]) possibles.add(i);
        
        Arrays.fill(check, false);
        for (int i = 1; i <= 9; i++) check[map[p.row][i]] = true; // row에 없는 숫자 찾기
        for (int i = 1; i <= 9; i++) if (!check[i]) possibles.add(i);
        
        Arrays.fill(check, false);
        for (int i = 1; i <= 9; i++) check[map[i][p.col]] = true; // col에 없는 숫자 찾기 
        for (int i = 1; i <= 9; i++) if (!check[i]) possibles.add(i);

        p.possible.addAll(possibles);
    }
    
    static class Point {
        int row;
        int col;
        List<Integer> possible = new ArrayList<>();
        
        Point (int row, int col) {
            this.row = row;
            this.col = col;
        }
    }
}

 

2. 풀이 중점 사항

 

제가 푼 풀이 코드가 많이 복잡하여 아쉬웠던 문제였습니다. 다른 분들이 해결한 코드를 보면 평균 400ms 정도로 매우 효율적이었지만 제 풀이는 1900ms 정도가 소요되었습니다.

 

static void setPossible(Point p) { // 가능한 숫자 찾기
    int blockRow, blockCol;
    Set<Integer> possibles = new HashSet<>();
    
    if (p.row <= 3) blockRow = 1;
    else if (p.row <= 6) blockRow = 4;
    else blockRow = 7;
    
    if (p.col <= 3) blockCol = 1;
    else if (p.col <= 6) blockCol = 4;
    else blockCol = 7;

    Arrays.fill(check, false);
    for (int i = blockRow; i < blockRow + 3; i++) { // block에 없는 숫자 찾기
        for (int j = blockCol; j < blockCol + 3; j++) check[map[i][j]] = true;
    }        
    for (int i = 1; i <= 9; i++) if (!check[i]) possibles.add(i);
    
    Arrays.fill(check, false);
    for (int i = 1; i <= 9; i++) check[map[p.row][i]] = true; // row에 없는 숫자 찾기
    for (int i = 1; i <= 9; i++) if (!check[i]) possibles.add(i);
    
    Arrays.fill(check, false);
    for (int i = 1; i <= 9; i++) check[map[i][p.col]] = true; // col에 없는 숫자 찾기
    for (int i = 1; i <= 9; i++) if (!check[i]) possibles.add(i);

    p.possible.addAll(possibles);
}

 

제가 풀이한 방법은 zero가 위치한 곳에서 0가 나올 수 있는 숫자의 경우를 set에 추가합니다.

예를 들어 1 행 1 열에 행, 열 , 블록을 확인해 보니 숫자 3, 4, 5가 없다면 해당 포인트의 possible에 3, 4, 5를 추가합니다. 

이렇게 zeros의 Point 객체들이 가질 수 있는 경우를 모두 set 한 후,

dfs()를 호출하여 백트래킹을 진행하였습니다.

 

 

static boolean isValid(int row, int col, int num) { // value가 있다면 종료
    Set<Integer> rowSet = hashMap.get(row);
    if (rowSet.contains(num)) return false;

    Set<Integer> colSet = hashMap.get(col * -1);
    if (colSet.contains(num)) return false;

    Set<Integer> blockSet = getBlockSet(row, col);
    return !blockSet.contains(num);
}

 

만약, value가 같은 row, col, block에 속해있는 Set에 추가하려는 문자가 없는 경우 map의 값을 업데이트하고, set에 추가하였습니다.  그리고 count를 증가하여 재귀 호출하였고 백트래킹으로 다시 map의 값을 0, set에서 값을 제거하였습니다.

 

map[p.row][p.col] = i; // 값 설정
Set<Integer> rowSet = hashMap.get(p.row);
rowSet.add(i); // rowSet에 추가

Set<Integer> colSet = hashMap.get(p.col * -1);
colSet.add(i); // colSet에 추가

Set<Integer> blockSet = getBlockSet(p.row, p.col);
blockSet.add(i); // 현재 row, col이 속한 block에 추가

dfs(count + 1);

if (sudokuBreak) return;
map[p.row][p.col] = 0; // 백트래킹
rowSet.remove(i);
colSet.remove(i);
blockSet.remove(i);

 

최종적으로 스도쿠를 완성하면, map을 출력하였습니다.

 

for (int i = 1; i <= 9; i++) {
    for (int j = 1; j <= 9; j++)
        sb.append(map[i][j]).append(" ");
    sb.append("\n");
}
System.out.print(sb);

 

 

3. 더 좋은 코드 

 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.StringTokenizer;
import java.util.stream.Collectors;

public class Main {

	static int[][] board = new int[9][9];

	public static void main(String[] args) throws IOException {

		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		for (int i = 0; i < 9; i++) {
			StringTokenizer st = new StringTokenizer(br.readLine());
			for (int j = 0; j < 9; j++) {
				board[i][j] = Integer.parseInt(st.nextToken());
			}
		}

		sudoku(0, 0);
	}

	private static boolean sudoku(int row, int col) {

		if (col == 9) {
			return sudoku(row + 1, 0);
		}

		if (row == 9) {
			printBoard();
			return true;
		}

		if (board[row][col] == 0) {
			for (int num = 1; num <= 9; num++) {
				if (isPossible(row, col, num)) {
					board[row][col] = num;
					if (sudoku(row, col + 1)) {
						return true;
					}
					board[row][col] = 0;
				}
			}
			return false;
		} else {
			return sudoku(row, col + 1);
		}
	}

	private static boolean isPossible(int row, int col, int num) {

		int rowStart = (row / 3) * 3;
		int colStart = (col / 3) * 3;

		for (int i = 0; i < 9; i++) {
			if (board[row][i] == num || board[i][col] == num || board[rowStart + (i / 3)][colStart + (i % 3)] == num) {
				return false;
			}
		}

		return true;
	}

	private static void printBoard() {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < 9; i++) {
			for (int j = 0; j < 9; j++) {
				sb.append(board[i][j]).append(" ");
			}
			sb.append("\n");
		}
		System.out.println(sb.toString());
	}
}

 

 

이상으로 스도쿠 자바 풀이를 마치도록 하겠습니다. 감사합니다!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 트리의 독립집합 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/2213

 

2213번: 트리의 독립집합

첫째 줄에 트리의 정점의 수 n이 주어진다. n은 10,000이하인 양의 정수이다. 1부터 n사이의 정수가 트리의 정점이라고 가정한다. 둘째 줄에는 n개의 정수 w1, w2, ..., wn이 주어지는데, wi는 정점 i의

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*;
import java.util.*;
import static java.lang.Integer.parseInt;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = parseInt(br.readLine());
        
        Graph graph = new Graph(n);
        StringTokenizer st = new StringTokenizer(br.readLine());
        for (int i = 1; i <= n; i++) graph.addWeight(i, parseInt(st.nextToken()));
        
        for (int i = 0; i < n - 1; i++) {
            st = new StringTokenizer(br.readLine());
            graph.addEdge(parseInt(st.nextToken()), parseInt(st.nextToken()));
        }

        graph.run();
    }    
}

class Graph {
    int n;
    Node[] nodes; // 노드 배열
    Queue<Node> queue = new PriorityQueue<>(Comparator.comparing((Node n) -> n.x)); // 독립 집합
    int[] dp, arr, selected; // dp, arr(집합의 가중치), 선택된 배열
    
    Graph (int n) {
        this.n = n;
        nodes = new Node[n + 1]; // 노드 배열
        dp = new int[n + 1];
        arr = new int[n + 1];
        selected = new int[n + 1];
        
        for (int i = 1; i <= n; i++) nodes[i] = new Node(i);
    }
    
    class Node {
        int x; // 노드 번호
        List<Node> adjacent = new ArrayList<>(); // 인접 노드
        List<Node> tree = new ArrayList<>(); // 트리
        Node (int x) {
            this.x = x;
        }
    }
    
    void addWeight (int x, int w) {
        arr[x] = w;
    }
    
    void addEdge(int x, int y) {  // 양뱡향 연관 관계
        Node n1 = nodes[x];
        Node n2 = nodes[y];
        
        n1.adjacent.add(n2);
        n2.adjacent.add(n1);
    }
    
    void run() {
        buildTree(nodes[1], nodes[0]);

        StringBuilder sb = new StringBuilder();
        int t1 = dp(nodes[1], 0); // 루트 포함 X
        int t2 = dp(nodes[1], 1); // 루트 포함
        
        if (t1 < t2) selected[1] = 1; // 만약 포함이 더 크다면 1
        else selected[1] = 0; // 아니라면 0
        sb.append(Math.max(t1, t2)).append("\n");
        
        trace(nodes[1], selected[1]); // 1번 노드에 대한 선택 여부로 추적
        
        while(!queue.isEmpty()) {
            sb.append(queue.poll().x).append(" "); // 큐의 노드 번호를 sb에 추가
        }

        System.out.println(sb);
    }

    void buildTree(Node now, Node parent) { // 트리 만들기, 현재 노드, 부모 노드
        for (Node child : now.adjacent) { // 현재 노드의 인접 노드
            if (child != parent) { // 부모 노드라면 무한 루프이므로 제거
                now.tree.add(child); // 트리에 추가
                buildTree(child, now); // 재귀 호출
            }
        }
    }
    
    int dp(Node now, int include) {
        int ans = 0;

        if (include == 1) {
            for (Node next : now.tree) ans += dp(next, 0); // 다음 노드는 포함하지 않음
            return ans + arr[now.x];
        } else {
            for (Node next : now.tree) {
                int t1 = dp(next, 0); // 현재 포함 안한 경우 다음에도 포함 안할 수 있음
                int t2 = dp(next, 1); // 다음은 포함하도록 
                
                if (t1 < t2) selected[next.x] = 1; // 포함이 더크면 포함 처리
                else selected[next.x] = 0; // 미포함 처리
                ans += Math.max(t1, t2);
            }
            return ans;
        }
    }
    
    void trace(Node now, int include) {
        if (include == 1) { // 현재를 포함했다면
            queue.add(now); // 큐에 추가하기
            for (Node next : now.tree) trace(next, 0); // 다음은 무조건 불가
        }
        else if (include == 0) {
            for (Node next : now.tree) trace(next, selected[next.x]); // 다음은 dp의 더 큰 값에 따라 달라짐
        }
    }
}

 

2. 풀이 중점 사항 

 

독립 집합은 그래프 이론에서 어떤 두 꼭짓점도 서로 인접하지 않는 그래프에 있는 꼭짓점들의 집합을 의미합니다. 이는 곳 간선으로 직접적으로 인접해 있지 않은 노드들을 의미합니다. 트리의 독립집합을 구하는 알고리즘은 일반적인 그래프에서 NP - 하드 문제로 알려져 있습니다.

 

이 문제를 해결하기 위해, 트리 구조, DP, 재귀의 방식이 사용 되었습니다.

저는 한 번에 해결하지 못하여, 하단의 블로그를 참고하였습니다.

 

주요 소스는 다음과 같습니다.

 

트리 

 

void buildTree(Node now, Node parent) { // 트리 만들기, 현재 노드, 부모 노드
    for (Node child : now.adjacent) { // 현재 노드의 인접 노드
        if (child != parent) { // 부모 노드라면 무한 루프이므로 제거
            now.tree.add(child); // 트리에 추가
            buildTree(child, now); // 재귀 호출
        }
    }
}

 

인접한 노드들 간에 트리 구조를 형성하기 위해 buildTree를 토대로 현재 노드, 부모 노드를 인자로 받았습니다.

만약 현재 노드의 인접한 노드 중 부모 노드가 다시 tree로 설정된다면 무한 루프에 빠질 수 있습니다. 따라서, 부모 노드를 제외하고 트리를 추가한 후 재귀로 호출하였습니다.

 

dp 

 

int dp(Node now, int include) {
    int ans = 0;

    if (include == 1) {
        for (Node next : now.tree) ans += dp(next, 0); // 다음 노드는 포함하지 않음
        return ans + arr[now.x];
    } else {
        for (Node next : now.tree) {
            int t1 = dp(next, 0); // 현재 포함 안한 경우 다음에도 포함 안할 수 있음
            int t2 = dp(next, 1); // 다음은 포함하도록 
            
            if (t1 < t2) selected[next.x] = 1; // 포함이 더크면 포함 처리
            else selected[next.x] = 0; // 미포함 처리
            ans += Math.max(t1, t2);
        }
        return ans;
    }
}

 

특정 스티커를 선택해야 할 때, 인접한 스티커를 선택하지 못하는 DP 문제가 있습니다. 이 소스도 그러한 원리와 비슷하다고 볼 수 있습니다. include는 현재 선택된 노드인지를 알려주는 지표로, 만약 선택했다면 다음 노드는 선택할 수 없습니다.

 

반면 현재 선택되지 않았다면, 다음에도 역시 선택 안 할 수 있고 선택할 수도 있습니다.

예를 들어 [1, 100, 10, 5, 200]이 있다면, 인접하지 않게 골라야 할 때, 100을 골랐다고 해서 반드시 10을 고를 필요는 없는 것과 동일합니다. 따라서, 두 가지 경우를 파악한 후 더 큰 곳에 포함 처리하도록 하였습니다.

 

 

이상으로 백준 트리의 독립 집합 자바 풀이를 마치도록 하겠습니다.

감사합니다!!!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 트리 DP 문제 트리와 쿼리 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/15681

 

15681번: 트리와 쿼리

트리의 정점의 수 N과 루트의 번호 R, 쿼리의 수 Q가 주어진다. (2 ≤ N ≤ 105, 1 ≤ R ≤ N, 1 ≤ Q ≤ 105) 이어 N-1줄에 걸쳐, U V의 형태로 트리에 속한 간선의 정보가 주어진다. (1 ≤ U, V ≤ N, U ≠ V)

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*;
import java.util.*;
import static java.lang.Integer.parseInt;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));    
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        int n = parseInt(st.nextToken());
        int r = parseInt(st.nextToken());
        int q = parseInt(st.nextToken());
        
        Graph graph = new Graph(n, r);
        for (int i = 0; i < n - 1; i++) {
            st = new StringTokenizer(br.readLine());
            graph.addEdge(parseInt(st.nextToken()), parseInt(st.nextToken()));           
        }
        
        for (int i = 0; i < q; i++) graph.addQuery(parseInt(br.readLine()));
        
        graph.makeSubTree(r); // 서브 트리 만들며 dp 저장
        graph.getPrint(); // 결과 출력
    }
}

class Graph {
    int n; // n개의 노드
    int root; // 루트 노드
    Node[] nodes; // 노드 저장
    List<Integer> querys = new ArrayList<>(); // 출력해야하는 쿼리
    int[] dp; // 서브 트리 개수
    
    Graph (int n, int root) {
        this.n = n;
        this.root = root;
        nodes = new Node[n + 1];
        dp = new int[n + 1];
        
        for (int i = 1; i <= n; i++) nodes[i] = new Node(i);
    }
    
    class Node {
        int x;
        List<Node> adjacent = new ArrayList<>(); // 인접 노드
        
        Node (int x) {
            this.x = x;
        }
    }
    
    void addEdge(int x, int y) {
        Node n1 = nodes[x];
        Node n2 = nodes[y];
        
        n1.adjacent.add(n2);
        n2.adjacent.add(n1);
    }
    
    void addQuery(int x) {
        querys.add(x);
    }
    
    void makeSubTree(int root) {
        dp[root] = 1; // 서브 트리에 포함
        Node node = nodes[root]; // 노드 꺼낸 후
        
        for (Node ad : node.adjacent) {
            if (dp[ad.x] == 0) { // 처음 호출 되는 경우만 재귀적 실행  --> dp == 0
                makeSubTree(ad.x); // 인접한 노드 여기서는 자식 노드를 의미
                dp[root] += dp[ad.x]; // 자식 노드의 서브트리를 더함 
            }
        }
    }
    
    void getPrint() {
        StringBuilder sb = new StringBuilder();
        
        for (Integer q : querys) {
            sb.append(dp[q]).append("\n");
        }
        
        System.out.print(sb);
    }
    
}

 

2. 풀이 중점 사항

 

문제를 출제하신 분께서 힌트로 너무나 자세하게 잘 설명해 주셔서 추가로 설명드릴 부분은 makeSubTree를 구현한 메서드입니다.

 

void makeSubTree(int root) {
    dp[root] = 1; // 서브 트리에 포함
    Node node = nodes[root]; // 노드 꺼낸 후
    
    for (Node ad : node.adjacent) {
        if (dp[ad.x] == 0) { // 처음 호출 되는 경우만 재귀적 실행  --> dp == 0
            makeSubTree(ad.x); // 인접한 노드 여기서는 자식 노드를 의미
            dp[root] += dp[ad.x]; // 자식 노드의 서브트리를 더함
        }
    }
}

 

makeSubTree를 만드는 과정에서 각 노드는 서로 인접한 노드로 설정되어 있습니다. 따라서, 각 노드에 대한 visited를 설정해주어야 합니다. 만약 방문 여부를 설정하지 않는 다면 인접한 노드가 다시 root 노드를 호출하기 때문에 무한 루프에 빠지게 됩니다.

저는 이 과정에서 dp[i]의 값이 0이라면, 아직 재귀 호출이 되지 않은 상태 즉, 자식 노드이므로 dp[i] == 0인 경우를 visited로 대체하여 사용하였습니다.

 

dp를 이용하여 한 번에 서브 트리의 개수를 구할 수 있었습니다.

이상으로 백준 트리와 쿼리 자바 풀이를 마치도록 하겠습니다. 감사합니다!! 

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 유니온 파인드 문제 - 최소 스패닝 트리 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/1197

 

1197번: 최소 스패닝 트리

첫째 줄에 정점의 개수 V(1 ≤ V ≤ 10,000)와 간선의 개수 E(1 ≤ E ≤ 100,000)가 주어진다. 다음 E개의 줄에는 각 간선에 대한 정보를 나타내는 세 정수 A, B, C가 주어진다. 이는 A번 정점과 B번 정점이

www.acmicpc.net

 

1. 풀이 소스

 

import java.util.*;
import java.io.*;
import static java.lang.Integer.parseInt;

public class Main {
    
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int v = parseInt(st.nextToken());
        int e = parseInt(st.nextToken());
        
        Graph graph = new Graph(v); // 그래프 설정
        for (int i = 0; i < e; i++) {
            st = new StringTokenizer(br.readLine());
            graph.addEdge(parseInt(st.nextToken()), parseInt(st.nextToken()), parseInt(st.nextToken()));
        }
        
        System.out.println(graph.mst());
    }
}

class Graph {
    int n; // n개의 노드
    int[] parent; // 유니온 파인드 루트 노드
    List<Edge> edges = new ArrayList<>(); // 간선
    
    Graph(int n) {
        this.n = n;
        parent = new int[n + 1];
        for (int i = 1; i <= n; i++) parent[i] = i;
    }
    
    class Edge {
        int x; // 연결된 노드
        int y; // 연결된 노드
        int w; // 가중치
        
        Edge (int x, int y, int w) {
            this.x = x;
            this.y = y;
            this.w = w;
        }
    }
    
    void addEdge(int x, int y, int w) {
        edges.add(new Edge(x, y, w));
    }
    
    int mst() {
        int result = 0;
        edges.sort(Comparator.comparing((Edge e) -> e.w)); // 엣지 오름차순 설정

        for (Edge e : edges) {
            int p1 = find(e.x); // x의 루트 노드
            int p2 = find(e.y); // y의 루트 노드

            if (p1 == p2) continue; // 만약 두 노드의 루트 노드가 같다면 패스
            result += e.w; // 가중치 업데이트
            union(p1, p2); // 두 노드 합치기
        }
        
        return result;
    }
    
    int find(int child) { // find 경로 압축
        if (child == parent[child]) return child;
        return parent[child] = find(parent[child]);
    }
    
    void union(int p1, int p2) { // union 합치기
        parent[p2] = p1;

    }
}

 

2. 최소 스패닝 트리(MST)

 

최소 스패닝 트리를 구하는 알고리즘은 Kruskal 알고리즘이 있습니다.

Kruskal 알고리즘은 다음의 순서로 진행됩니다.

  • 그래프의 모든 간선을 가중치에 따라 오름차순 정렬합니다.
  • 가중치가 최소인 간선을 선택하여, 간선의 두 노드가 같은 집합에 있지 않는 경우 (루트가 다른 경우) 해당 간선에 연결된 두 노드를 하나의 집합으로 합칩니다 (유니온 파인드)
  • 만약 간선이 연결하는 두 노드가 같은 집합이라면 사이클이 형성될 수 있으므로 건너뜁니다.
  • 모든 간선이 검사 될 때까지 과정을 반복합니다.

     

        7

1 ------------- 2

                    | 

                    |  10  

                    |

                    3 -------------- 4

                              5

 

다음과 같은 트리가 존재한다고 가정하겠습니다.

  • Kruskal 알고리즘의 순서에 따라 가중치가 가장 적은 (3, 4) 엣지, (1, 2) 엣지, (2, 3) 엣지로 정렬합니다.
  • (3, 4)에 있는 노드 3, 4의 루트 노드를 검사합니다. (아직 업데이트되지 않았기에 3의 루트 노드 3, 4의 루트 노드 4)
  • 두 루트 노드가 다르므로 하나로 합칩니다. 저는 x1, x2가 있을 때, x1을 루트 노드로 설정하였습니다.
  • 마찬가지로 (1, 2) 엣지를 선택하여 두 노드에 대한 유니온 파인드를 진행합니다.
  • 다음 엣지인 (2, 3)을 선택하여 두 노드에 대한 유니온 파인드를 진행합니다.
  • 이 과정에서 분리되어 있던 두 집합 {1, 2} , {3, 4}가 하나로 합쳐지게 됩니다.
  • 모든 간선에 대한 탐색을 마쳤으므로 종료합니다.

만약 해당 그래프에서 

 

        7

1 ------------- 2

  \                  | 

   \                 |  10  

    \                |

     \------------ 3 -------------- 4

   100                      5

 

1과 3을 잇는 간선이 추가될 경우 사이클을 형성할 수 있습니다.

유니온 파인드에서 두 루트 노드가 같은 경우에는 건너뛰는 이유가 바로 이러한 사이클을 방지하기 위함입니다.

이미, 간선으로 오름차순 설정하였기 때문에 후에 오는 간선은 이전에 설정한 간선보다 더 작기 때문에
그리디한 탐색 방법이 MST에서는 최적의 해를 찾을 수 있는 것입니다.

 

int mst() {
    int result = 0;
    edges.sort(Comparator.comparing((Edge e) -> e.w)); // 엣지 오름차순 설정

    for (Edge e : edges) {
        int p1 = find(e.x); // x의 루트 노드
        int p2 = find(e.y); // y의 루트 노드

        if (p1 == p2) continue; // 만약 두 노드의 루트 노드가 같다면 패스
        result += e.w; // 가중치 업데이트
        union(p1, p2); // 두 노드 합치기
    }
    
    return result;
}

 

이러한 원리를 바탕으로 소스를 보면, 먼저 엣지의 가중치를 기준으로 정렬하고,

각 간선을 탐색합니다. 엣지에 대한 노드를 유니온 파인드를 진행하고 result에 가중치를 더해줍니다

나중에는 오름차순에 따라 간선이 나오게 되며 결국 분리되어 있는 여러 개의 집합이 하나로 합쳐지기 때문에 이러한 방법을 택할 수 있습니다.

 

이상으로 백준 최소 스패닝 트리 자바 풀이를 마치도록 하겠습니다. 감사합니다!

+ Recent posts