20210번 파일 탐색기

 

문자열 유형을 혼내주고 있는 도중 내가 혼나버렸다.

문제를 골랐는데, 항상 정답률을 보고 적당한 문제들을 골랐었는데 하하 왜 안 보고 골랐을까.

일단 골라버렸고, 코드는 작성해버렸고 문제를 풀었다.

 


1. 문제 분석


문제를 처음 보고나서 조건이 되게 많다는 것을 느꼈다.

얼핏 보기에는 뭔가 간단하게 그냥 조건 따라서 정렬하면 되는 문제라고 판단됐다. 그래서 선택했을지도...

 

  1. 둘 다 숫자인 경우
  2. 둘 중 하나만 숫자인 경우
  3. 둘 다 숫자가 아닌 경우

크게 이렇게 3가지 경우로 나눌 수 있었다.

 

  1. 둘 다 숫자라면?
    • 십진법으로 두 숫자의 크기를 비교한다.
    • 더 작은 숫자가 우선순위가 더 높다.
    • 만약 두 숫자의 크기가 같다면, 앞에 0의 개수가 적은 것이 우선순위가 더 높다.
    • 두 숫자의 크기도 같고, 앞에 0의 개수도 동일하다면 우선순위가 동일하다. (저는 여기서...시간낭비를...)
  2. 둘 중 하나만 숫자라면?
    • 숫자가 문자보다 우선순위가 높다.
  3. 둘 다 문자라면?
    • 문자의 우선순위는 다음과 같은 우선순위를 갖는다.
    • 두 문자가 같으면(A와 A) 우선순위가 동일하다.
    • 두 문자의 종류가 같으면(A와 a) 대문자가 우선순위가 높다.
    • 두 문자의 종류가 다르면(A와 c) AaBbCcDd....XxYyZz와 같은 우선순위를 갖는다. (문제 제대로 안 읽으면 여기서 고생)

또 고려해야 할 것이 있다. 이어져있는 숫자를 하나의 문자열로 사용을 하는데, 주어지는 숫자는 2^63을 초과할 수 있기 때문에 일반적인 Long 타입은 사용할 수 없다.

 

문제를 다 풀고 나서 다른 분들의 답을 봤는데, 반복문과 Index를 아주 매력적으로 사용해서 푸신 분들을 많이 봤는데... 존경스럽다.

나는 아주 큰 수도 저장할 수 있는 Java의 BigDecimal을 사용했다.

 

왜냐! BigDecimal은 Comparable 인터페이스를 구현하고 있어서 compareTo를 사용할 수 있기 때문에 사용해 봤다.

 

 

 

2. 소스코드


import java.io.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.PriorityQueue;

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

        int N = Integer.parseInt(reader.readLine());
        PriorityQueue<Value> queue = new PriorityQueue<>();

        for (int i = 0; i < N; i++) {
            queue.add(new Value(reader.readLine()));
        }

        StringBuilder builder = new StringBuilder();
        while (!queue.isEmpty()) {
            builder.append(queue.poll()).append("\n");
        }
        System.out.println(builder);
    }
}

class Value implements Comparable<Value> {
    String value;
    ArrayList<String> seperatedValue;

    public Value(String value) {
        this.value = value;
        setSeperatedValue(value);
    }

    private void setSeperatedValue(String value) {
        this.seperatedValue = new ArrayList<>();
        StringBuilder tempBuilder = new StringBuilder();

        for (int i = 0; i < value.length(); i++) {
            char temp = value.charAt(i);

            if (Character.isDigit(temp)) {
                tempBuilder.append(temp);
                continue;
            } else if (tempBuilder.length() > 0) { // 숫자 아니고, 빌더에 값이 있는 경우
                seperatedValue.add(tempBuilder.toString());
                tempBuilder = new StringBuilder();
            }
            seperatedValue.add(String.valueOf(temp));
        }

        if (tempBuilder.length() > 0) { // 다 돌고 빌더에 남아 있는 것이 있으면 마저 넣어줌
            seperatedValue.add(tempBuilder.toString());
        }
    }

    @Override
    public int compareTo(Value other) {
        ArrayList<String> seperatedValueOfOther = other.seperatedValue;
        int smallSize = Math.min(seperatedValueOfOther.size(), this.seperatedValue.size());
        for (int i = 0; i < smallSize; i++) {
            String valueOfThis = this.seperatedValue.get(i);
            String valueOfOther = seperatedValueOfOther.get(i);

            boolean isNumberOfThis = isNumber(valueOfThis);
            boolean isNumberOfOther = isNumber(valueOfOther);

            // 둘 다 숫자인 경우
            if (isNumberOfThis && isNumberOfOther) {
                BigDecimal decimalOfThis = convertToDecimal(valueOfThis);
                BigDecimal decimalOfOther = convertToDecimal(valueOfOther);

                if (decimalOfThis.equals(decimalOfOther)) {
                    // 값도 같고, 0의 개수도 같은 경우
                    if (valueOfThis.length() == valueOfOther.length()) {
                        continue;
                    }
                    return valueOfThis.length() - valueOfOther.length();
                }
                return decimalOfThis.compareTo(decimalOfOther);
            }

            // 둘 중 하나만 숫자인 경우
            if (isNumberOfThis || isNumberOfOther) {
                return isNumberOfThis ? -1 : 1;
            }

            // 둘 다 문자인 경우
            int comparedValue = compare(valueOfThis, valueOfOther);

            if (comparedValue == 0) {
                continue;
            }

            return comparedValue;
        }

        return this.value.length() - other.value.length();
    }

    private int compare(String value1, String value2) {
        if (value1.equals(value2)) {
            return 0;
        }
        if (value1.equalsIgnoreCase(value2)) {
            if (value1.toUpperCase().compareTo(value1) == 0) {
                return -1;
            }
            return 1;
        }

        return value1.compareToIgnoreCase(value2);
    }

    private boolean isNumber(String value) {
        return Character.isDigit(value.charAt(0));
    }

    private BigDecimal convertToDecimal(String value) {
        return new BigDecimal(value);
    }

    @Override
    public String toString() {
        return value;
    }
}

 

처음에 BigDecimal의 생성자를 무자비하게 그냥 사용했었는데, 속도가 많이 떨어지는 것을 느꼈다.

 

그래서 isNumber()라는 메서드를 분리해서 해당 문자열이 숫자인지 확인한 후에 숫자인 경우에만 BigDecimal을 사용하도록 변경했더니 속도가 어느 정도 개선은 됐다.

 

그래도 확실히 속도는 조금 떨어지는 것 같아 보이기는 했다. 집념으로... 풀어서 그래도 오늘 문제 해결해서 잠들 수는 있을 거 같다.

 

집념의 흔적....isNumber()를 적용한 후에 1848ms에서 1384ms까지 속도가 올라가는 것을 확인할 수 있었다.

오랜만에 알고리즘이다... 알고리즘은 꾸준히 해야 하는데, 이 꾸준히라는 게 너무 어려운 것 같다.

 

코딩 테스트를 몇 번 보면서 느낀점이 있었다. "문자열" 문제 진짜 많이 나오는데, 아슬아슬하게 맨날 못 풀고 있는 것 같았다.

그래서 문자열 유형을 한번 혼내주기로 했다.

 

 

16934번 게임 닉네임


1. 문제 분석


문자열의 Prefix를 확인해야 하고, 이미 포함되어 있는지에 대한 여부도 확인하면서 문제에 접근해야 했다.

여기서 생각났던 알고리즘은 "트라이" 알고리즘이다. (사실 써보고 싶었습니다.)

 

왜 "트라이"인가? 새롭게 추가되는 문자열을 이미 추가되어 있는 문자열의 prefix에 해당하는지 확인을 해야 한다.

문자열 접두어 확인을 할 때는 "트라이" 알고리즘을 선택하면 빠르게 해결할 수 있다.

 

트라이의 생성 시간 복잡도는 주어지는 닉네임 개수(N, 최대 100,000)와 최대 문자열 길이(L, 10)를 고려해 보면 O(N*L)의 시간복잡도로 트라이를 만들 수 있고, 탐색에서는 O(L)의 시간복잡도로 탐색을 할 수 있어서 빠른 생성과 탐색이 가능한 알고리즘이다. (혁-신)

 

 

 

2. 소스코드


import java.io.*;
import java.util.*;

public class Main {
    private static final String NEW_LINE = "\n";
    private static final String BLANK = "";

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

        Trie trie = new Trie();
        NicknameCounts nicknameCounts = new NicknameCounts();
        StringBuilder answer = new StringBuilder();

        int N = Integer.parseInt(reader.readLine());
        for (int i = 0; i < N; i++) {
            String nickname = reader.readLine();
            nicknameCounts.insert(nickname);

            int matchedLastIndex = trie.searchMatchLastIndex(nickname);
            if (matchedLastIndex == nickname.length() - 1) { // nickname 전체가 prefix로 존재
                int x = nicknameCounts.getCount(nickname);
                answer.append(nickname).append(x > 1 ? x : BLANK).append(NEW_LINE);
            } else if (matchedLastIndex > -1) { // trie 안에 겹치는 prefix 존재
                answer.append(nickname, 0, matchedLastIndex + 2).append(NEW_LINE);
            } else if (matchedLastIndex == -1) { // trie 안에 겹치는 prefix 없음
                answer.append(nickname.charAt(0)).append(NEW_LINE);
            }

            trie.insert(nickname);
        }
        System.out.println(answer);
    }
}

class TrieNode {
    Map<Character, TrieNode> childNodes;
    boolean isTerminated;

    public TrieNode() {
        this.childNodes = new HashMap<>();
        this.isTerminated = false;
    }
}

class Trie {
    TrieNode rootNode;

    public Trie() {
        this.rootNode = new TrieNode();
    }

    public void insert(String word) {
        TrieNode node = rootNode;
        for (char ch : word.toCharArray()) {
            node = node.childNodes.computeIfAbsent(ch, key -> new TrieNode());
        }
        node.isTerminated = true;
    }

    public int searchMatchLastIndex(String word) {
        int index = -1;
        TrieNode node = rootNode;
        for (char ch : word.toCharArray()) {
            node = node.childNodes.getOrDefault(ch, null);

            if (node == null) {
                break;
            }
            index++;
        }
        return index;
    }
}

class NicknameCounts {
    private final Map<String, Integer> nicknameCounts;

    public NicknameCounts() {
        this.nicknameCounts = new HashMap<>();
    }

    public void insert(String nickname) {
        if (nicknameCounts.containsKey(nickname)) {
            nicknameCounts.put(nickname, nicknameCounts.get(nickname) + 1);
            return;
        }
        nicknameCounts.put(nickname, 1);
    }

    public int getCount(String nickname) {
        return nicknameCounts.getOrDefault(nickname, 0);
    }
}

 

  1. 입력된 nickname 전체가 이전에 등록된 닉네임의 prefix로 존재하는 경우
  2. 입력된 nickname 일부가 이전에 등록된 닉네임의 prefix로 존재하는 경우
  3. 입력된 nickname이 이전에 등록된 닉네임의 prefix로 존재하지 않는 경우

위와 같이 3개의 경우로 나눠서 코드를 작성해봤습니다.

 

(1) 번의 경우 입력된 nickname 전체가 이미 다른 닉네임의 prefix로 존재하기 때문에 동일한 nickname의 개수가 있는지 확인해 보고, 만약 동일한 nickname의 개수가 1보다 큰 경우 "nickname{개수}"를 붙여서 별칭으로 지정해 줍니다. 만약 1보다 작다면 그냥 본인의 닉네임 자체를 별칭으로 사용합니다.

 

(2) 번의 경우 입력된 nickname의 일부가 이미 다른 닉네임의 prefix로 존재하기 때문에 matchedLastIndex를 사용해서 substring을 통해 가장 짧은 prefix를 만들어냈습니다.

 

예를 들어 "abcd"가 이미지 저장되어 있고, "abdef"가 입력된 닉네임이라면 matchedLastIndex는 1이 될 것입니다. 그럼 여기서 가장 짧은 prefix를 잘라내려면 "abd"가 되어야 하는데, substring(0, 1 + 2)으로 잘라낼 수 있습니다.

 

(3) 번의 경우 nickname이 이전에 등록된 어떤 닉네임의 prefix와도 겹치지 않기 때문에 그냥 자신의 nickname을 그대로 별칭으로 사용하면 됩니다.

 

 

 

 

트라이를 최근에 동아리에서 CS 스터디하면서 알게 되었는데, 그때 검색에서 많이 사용되는 알고리즘이라고 해서 한번 문제에 직접 사용해보고 싶었는데, 마침 문자열 문제에 사용할 수 있게 돼서 재미(?) 있었습니다.

 

다시... 백준 스트릭을 연속으로 채우는 그날까지... 화이팅

 

 

 

1918 후위 표기식

 

1. 문제 분석


  • 연산자의 우선순위를 생각해야 하는 문제입니다.
  • 주의해야 할 점은 A + B + C의 경우에는 앞에 있는 +가 우선순위가 더 높기 때문에 ABC++가 아닌 (A + B) + C로 묶어서 AB+C+가 나와야 합니다.
  • 그리고 추가로 괄호로 묶여서 제공되는 연산자의 경우에는 가장 우선순위가 높기 때문에 가장 먼저 처리해줘야 합니다.
  • 알파벳에 해당하는 (A ~ Z)는 바로 문자열에 추가하고, 연산자(+, -, *, /)는 Stack에 저장해야 합니다.

 

 

 

2. 핵심 아이디어


  • Stack에는 연산자 들이 저장됩니다. 즉 먼저 들어온 연산자들은 아래쪽에 쌓이게 됩니다.
  • 첫 번째 아이디어는 "("가 스택에 들어왔을 때입니다.

    • "("는 일단 스택에 넣어줍니다. 그다음 연산자를 받아주고, 닫는 괄호 ")"가 들어왔을 때, 괄호 내부에 있는 연산자를 뽑아주는 작업을 해야 합니다.
    • 그 이유는 괄호 안에 있는 연산자가 어떤 연산자인지 상관없이 우선순위가 높기 때문입니다.
    • 이때는 while에 조건을 넣어서 "("가 나올 때까지 pop을 해서 뽑아내서 정답 문자열 뒤에 붙여줍니다. 이때 "("는 정답에 포함되면 안 됩니다.

  • 두 번째 아이디어는 이전에 스택에 들어온 연산자 중에서 나보다 우선순위가 높은 연산자가 있는지 확인해서 추출해야 합니다.

    • 후위 표기식에서는 알파벳 뒤에는 우선순위가 높은 순서로 연산자가 붙습니다.
    • A + B * C 라면 우선순위가 높은 순서로 ABC*+ 이렇게 붙게 됩니다.
    • 그래서 이미 스택에 들어온 연산자들 중에서 내가 현재 가지고 있는 연산자 보다 높은 우선순위가 존재하면 안 됩니다.

  • 세 번째 아이디어는 수식의 모든 순회를 끝냈을 때, Stack에 남아있는 연산자들을 순서대로 추출해서 정답 문자열에 더해줘야 합니다.

 

 

 

3. 코드 작성


import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String notation = br.readLine();
        Stack<Character> operator = new Stack<>();
        StringBuilder builder = new StringBuilder();

        for (int i = 0; i < notation.length(); i++) {
            char value = notation.charAt(i);
            if (value >= 'A' && value <= 'Z') {
                builder.append(value);
            } else if (value == '(') {
                operator.add(value);
            } else if (value == ')') {
                // 닫는 괄호가 나오면 여는 괄호가 나올 때까지 빼서 연산자만 추출한다. (괄호안에 연산자가 우선순위가 있기 떄문에)
                while (!operator.isEmpty()) {
                    char popVal = operator.pop();
                    if (popVal == '(') {
                        break;
                    }
                    builder.append(popVal);
                }
            } else {
                // 괄호가 아닌 경우에는 이전 연산자가 현재 연산자보다 우선순위가 높은지 확인 후 추출
                while (!operator.isEmpty() && checkPriority(operator.peek(), value)) {
                    builder.append(operator.pop());
                }
                operator.add(value);
            }
        }
        // 남아있을 수 있는 부분이 있어서 나머지 모두 추출해서 뒤에 붙여줌
        while (!operator.isEmpty()) {
            builder.append(operator.pop());
        }
        System.out.println(builder);
    }

    public static boolean checkPriority(char peekVal, char val) {
        int peekValWeight = setWeight(peekVal);
        int valWeight = setWeight(val);
        // 같은 순위의 연산자라면 먼저 나온 것이 우선순위가 크기 때문에 등호가 필수
        return peekValWeight >= valWeight;
    }

    public static int setWeight(char val) {
        if (val == '(') return 0;
        return val == '+' || val == '-' ? 1 : 2;
    }
}

2206번 벽 부수고 이동하기

 

1. 문제 분석


  • 0은 이동할 수 있는 칸, 1은 벽이 있어서 이동할 수 없는 칸입니다.
  • (1, 1)에서 시작해서 (N, M)까지 가는데 최단 경로를 구해야 합니다.
  • 이동은 상, 하, 좌, 우로 가능하고, 이때 벽(1)을 최소 한번 부수고 지나갈 수 있습니다.
  • 문제에서 주어진 사항을 보고, BFS로 접근해야겠다는 생각을 했습니다.
  • 방향이 존재하기 때문에 재귀적으로 구현을 했고, 벽을 1번 이상 부순 경우는 더 이상 나아가지 못하도록 막는 방법을 생각했습니다.
  • 계속 틀렸다는 말이 나와서 2차원 배열 visited를 이용하여 문제를 변경해서 풀어봤는데, 이때는 시간 초과가 났습니다.

 

 

 

2. 핵심 아이디어


  • 특정한 지점 (x, y)에 도달하는 데 있어서 벽을 1번 부수고 가는 경우벽을 아예 부수지 않고 가는 경우 중에서 벽을 1번 부수고 가는 경우가 더 빠를 수 있습니다.
  • 특정 지점 (x, y)까지의 결과는 위와 같지만, 최종 (N, M)까지 도달하는 데 있어서는 벽을 아예 부수지 않고 가는 경우가 더 빠를 수 있습니다.

 

  • 위 그림 처럼 파란 지점까지 벽을 한 번 부수고 가면 3번이면 가는데, 벽을 부수지 않고 가면 13번이 걸립니다.
  • 하지만 벽을 한번 부수고 3번으로 지나가면 (N, M)에 도달하지 못하고 정답이 될 수 없습니다.
  • 파란 지점에 벽을 한번 부순것이 먼저 도달해서 visited[1][3]을 true로 만들었고, 벽을 부수지 않고 지나가는 경우에 같은 vistied를 사용하게 된다면 해당 경우는 도달할 수 있는 경우가 -1이 됩니다.
  • 즉, 벽을 부수고 지나가는 경우에 대한 visited와 벽을 부수지 않고 지나가는 경우에 대한 visited를 나눠서 처리해줘야 합니다.
  • 이 문제에서 2차원 배열을 2개 만드는 것 보다 visited[][][]를 3차원 배열로 만들어서 visited[row][col][0]은 벽을 부수고 지나가는 경우에 대한 visited로 사용하고, visited[row][col][1]은 벽을 부수지 않고 지나가는 경우에 대한 visited로 사용하면 쉽게 처리할 수 있습니다.

 

 

 

3. 소스코드


import java.io.*;
import java.util.*;

public class Main {
    static int N, M;
    static int[][] map;
    static boolean[][][] visited;
    static int[][] move = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
    
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());
        map = new int[N + 1][M + 1];
        visited = new boolean[N + 1][M + 1][2]; // (벽을 부수고 가는 경우, 벽을 부수지 않고 가는 경우)
        for (int row = 1; row <= N; row++) {
            char[] input = br.readLine().toCharArray();
            for (int col = 1; col <= M; col++) {
                map[row][col] = input[col - 1] - '0';
            }
        }
        int answer = bfs();
        System.out.println(answer);
    }

    public static int bfs() {
        Queue<Node> nodes = new LinkedList<>();
        nodes.add(new Node(1, 1, 1, false));
        visited[1][1][1] = true;

        while (!nodes.isEmpty()) {
            Node curr = nodes.poll();

            if (curr.row == N && curr.col == M) {
                return curr.dist;
            }

            int nextDist = curr.dist + 1;
            for (int[] m : move) {
                int nextRow = curr.row + m[0];
                int nextCol = curr.col + m[1];
                if (!isValid(nextRow, nextCol)) {
                    continue;
                }

                if (map[nextRow][nextCol] == 1) { // 벽인 경우
                    if (!curr.useBreak && !visited[nextRow][nextCol][0]) { // 아직 부순적이 없는 경우
                        nodes.add(new Node(nextRow, nextCol, nextDist, true));
                        visited[nextRow][nextCol][0] = true;
                    }
                } else if (map[nextRow][nextCol] == 0) { // 벽이 아닌 경우
                    if (curr.useBreak && !visited[nextRow][nextCol][0]) { // 벽을 부수고 가는 경우
                        nodes.add(new Node(nextRow, nextCol, nextDist, true));
                        visited[nextRow][nextCol][0] = true;
                    } else if (!curr.useBreak && !visited[nextRow][nextCol][1]) { // 벽을 부수지 않고 가는 경우
                        nodes.add(new Node(nextRow, nextCol, nextDist, false));
                        visited[nextRow][nextCol][1] = true;
                    }
                }
            }
        }
        return -1;
    }

    public static boolean isValid(int row, int col) {
        return row >= 1 && col >= 1 && row <= N && col <= M;
    }

    static class Node {
        int row, col, dist;
        boolean useBreak;

        public Node (int row, int col, int dist, boolean useBreak) {
            this.row = row;
            this.col = col;
            this.dist = dist;
            this.useBreak = useBreak;
        }
    }
}

 

위 코드에서 핵심적인 부분은 다음과 같습니다.

if (map[nextRow][nextCol] == 1) {
    if (!curr.useBreak && !visited[nextRow][nextCol][0]) {
        nodes.add(new Node(nextRow, nextCol, nextDist, true));
        visited[nextRow][nextCol][0] = true;
    }
} else if (map[nextRow][nextCol] == 0) {
    if (curr.useBreak && !visited[nextRow][nextCol][0]) {
        nodes.add(new Node(nextRow, nextCol, nextDist, true));
        visited[nextRow][nextCol][0] = true;
    } else if (!curr.useBreak && !visited[nextRow][nextCol][1]) {
        nodes.add(new Node(nextRow, nextCol, nextDist, false));
        visited[nextRow][nextCol][1] = true;
    }
}
  • 만약 다음에 내가 가야할 곳이 벽(1)이라면

    1. 내가 Break를 사용했는지 판단합니다. (curr.useBreak가 false인지 확인)
    2. 벽을 부수고 지나가는 경우(visited[][][0])에 이미 방문된 칸인지 확인합니다.
    3. Break을 이미 사용한 경우에 벽(1)을 만나면 Queue에는 아무런 노드도 넣지 않습니다. (이미 끝)

  • 만약 다음에 내가 가야할 곳이 벽이 아니라면(0)

    1. 내가 Break를 사용했는지 판단합니다.
    2. 만약 이미 Break를 사용했다면 visited[][][0]의 방문처리 상태를 확인합니다.
    3. 아직 Break를 사용하지 않았다면 visited[][][1]의 방문처리 상태를 확인합니다.

이렇게 문제에 접근해볼 수 있습니다. 사실 핵심 아이디어만 떠올렸다면 절대 어렵지 않은 문제였던 거 같습니다. 벽을 부술 수 있는 횟수가 정해져 있기 때문에 이런 식으로 접근해 볼 수 있을 거 같습니다.

 

visited를 3차원 배열로...메..모...

 

1967번 트리의 지름

 

1. 문제 접근


  • 특정 노드 2개를 선택해서 쭉 늘렸을 때, 가장 길이가 긴 두 노드를 찾아야 한다. 이 길이를 문제에서는 트리의 지름이라고 칭합니다.
  • 1번 노드부터 탐색해서 가장 가중치가 큰 노드 하나를 선택합니다.
  • 가장 가중치가 큰 노드는 반드시 트리의 지름의 한 점이 된다고 생각했습니다.
  • 가장 가중치가 큰 노드를 하나 선택했으면, 해당 노드부터 BFS로 다시 탐색을 시작해서 가장 길이가 긴 값(트리의 지름)을 반환합니다.

 

 

 

2. 접근 과정에서 실수했던 부분


  • 문제에서 "간선에 대한 정보는 부모 노드의 번호가 작은 것이 먼저 입력되고, 부모 노드의 번호가 같으면 자식 노드의 번호가 작은 것이 먼저 입력된다."라는 부분이 있었습니다.
  • 이 부분을 잘못 이해해서 다음과 같이 문제 접근을 시작했습니다.

 

(1) 입력을 받으면서 가장 가중치가 큰 노드를 선택

Node[] nodes = new Node[n + 1]; // 가장 가중치가 큰 노드를 찾기 위한 배열
nodes[0] = new Node(0, 0); // NullPointerException 방지를 위해
nodes[1] = new Node(1, 0); // 1번은 루트이기 때문에 가중치가 0

for (int i = 1; i < n; i++) {
    StringTokenizer st = new StringTokenizer(br.readLine());
    int first = Integer.parseInt(st.nextToken());
    int second = Integer.parseInt(st.nextToken());
    int weight = Integer.parseInt(st.nextToken());
    map[first].add(new Node(second, weight)); // 트리의 양방향 설정
    map[second].add(new Node(first, weight)); // 트리의 양방향 설정
    int beforeWeight = nodes[first] == null ? 0 : nodes[first].weight;
    nodes[second] = new Node(second, beforeWeight + weight);
}

Arrays.sort(nodes, (n1, n2) -> n2.weight - n1.weight);
// 도달 거리가 가장 큰 놈을 하나 선택
int start = nodes[0].end;
  • O(N)에 모든 것을 다 챙겨가려는 오만한 생각에 코드를 이렇게 짰습니다.
  • 이렇게 해서 문제 제출을 해보니 67%쯤에서 계속 틀렸다고 나왔습니다.

 

 

(2) 도움이 됐던 반례

  • 이진 트리라는 조건이 없어서 생겼던 웬만한 반례들은 모두 통과가 됐습니다.
  • 그래서 처음에는 어느 부분에서 실수를 한지 판단할 수가 없어서 계속 생각해 보다가 알고리즘에 도움을 줬던 반례가 있었습니다.
12
1 3 3
1 12 2
2 5 1
2 11 7
3 2 50
4 8 15
4 9 4
6 7 6
6 10 10
12 4 11
12 6 9

정답 : 88
  • 해당 반례는 딱 제가 실수했던 부분에 대한 반례였습니다.
  • "간선에 대한 정보는 부모 노드의 번호가 작은 것이 먼저 입력되고, 부모 노드의 번호가 같으면 자식 노드의 번호가 작은 것이 먼저 입력된다."라는 조건은 확실하게 만족하는 입력 값들이었습니다.
  • 제 알고리즘대로라면 {2 5 1}이 입력되었을 때, 5라는 노드의 가중치는 2라는 노드의 가중치에 1을 더한 값이 세팅되어야 합니다. 하지만 위 입력에서는 아직 노드 2에 대한 제대로 된 가중치가 저장되기 전이기 때문에 옳지 않은 정답을 출력한 것을 알 수 있습니다.

 

 

 

3. 문제 해결


  • 기존의 나머지 알고리즘은 그대로 두고, 문제가 있던 입력받는 부분에 대해서만 수정을 했습니다.
  • 입력은 그대로 인접 리스트를 사용하여 양방향 정보를 저장하고, BFS 탐색을 2번 했습니다.
  • 첫 번째 탐색에서는 가중치의 최대값을 갱신하면서 그 최대 가중치를 갖는 노드(start)를 찾아냈습니다.
  • 두 번째 탐색에서는 최대 가중치를 갖는 노드(start)부터 다시 탐색을 시작해서 가장 가중치가 긴 값(트리의 지름)을 갱신해서 반환했습니다.
import java.io.*;
import java.util.*;

public class Main {

    static int n, start;
    static List<Node>[] map;
    static boolean[] visited;

    public static void main(String[] args) throws IOException {
        System.setIn(new FileInputStream("src/main.txt"));
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        n = Integer.parseInt(br.readLine());
        map = new ArrayList[n + 1];
        for (int i = 0; i <= n; i++) {
            map[i] = new ArrayList<>();
        }

        for (int i = 1; i < n; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int first = Integer.parseInt(st.nextToken());
            int second = Integer.parseInt(st.nextToken());
            int weight = Integer.parseInt(st.nextToken());
            map[first].add(new Node(second, weight));
            map[second].add(new Node(first, weight));
        }
        // 가장 큰 노드를 탐색
        visited = new boolean[n + 1];
        bfs(1);

        // 해당 노드부터 탐색을 시작해서 최장 거리를 찾음
        visited = new boolean[n + 1];
        int result = bfs(start);
        System.out.println(result);
    }

    public static int bfs(int startNodeNumber) {
        int result = 0;
        Queue<Node> queue = new LinkedList<>();
        queue.add(new Node(startNodeNumber, 0));
        visited[startNodeNumber] = true;

        while (!queue.isEmpty()) {
            Node curr = queue.poll();
            if (result < curr.weight) {
                result = curr.weight;
                start = curr.end;
            }

            for (Node node : map[curr.end]) {
                if (visited[node.end]) continue;
                queue.add(new Node(node.end, curr.weight + node.weight));
                visited[node.end] = true;
            }
        }

        return result;
    }

    static class Node {
        int end;
        int weight;

        public Node(int end, int weight) {
            this.end = end;
            this.weight = weight;
        }
    }
}

1238번 파티 문제 풀러 가기

 

1238번: 파티

첫째 줄에 N(1 ≤ N ≤ 1,000), M(1 ≤ M ≤ 10,000), X가 공백으로 구분되어 입력된다. 두 번째 줄부터 M+1번째 줄까지 i번째 도로의 시작점, 끝점, 그리고 이 도로를 지나는데 필요한 소요시간 Ti가 들어

www.acmicpc.net

 

 

1. 문제 분석


  • N명의 학생들은 X번 마을까지 갔다가 다시 돌아와야 한다.
  • 이때 갔다가 다시 돌아오는 최단 거리를 구해야 한다.
  • 각 마을에서 마을로 이동하는 데는 양의 가중치(거리)가 존재한다.

 

 

 

2. 생각해 보기


  • 양의 가중치가 존재하기 때문에 다익스트라 아니면 플로이드 와샬을 고려해 본다.
  • 플로이드 와샬 알고리즘은 O(N^3)의 시간 복잡도가 필요하다. 마을의 개수(N)가 최대 10^3이기 때문에 플로이드 와샬을 사용하면 최대 10^9의 시간이 걸릴 수 있어 시간 초과가 날 가능성이 있다.
  • 그럼 고려해 볼 수 있는 알고리즘은 다익스트라 알고리즘이다.
  • 다음은 각 마을에서 X로 가는 최단 거리와, X에서 다시 각 마을로 돌아가는 최단 거리를 구해야 한다.
  • 그대로 구현한다면 1, 3, 4 마을에서 각각 2번 마을까지 갈 수 있는 거리를 구해야 한다. 이를 반대로 생각해 보면 다음과 같이 접근해 볼 수 있다.
     
    1. (시작 마을) (끝 마을) (거리)를 입력받을 때, 입력받은 대로 저장하는 경우 map과 (끝 마을) (시작 마을) (거리)로 저장하는 경우reverseMap을 모두 저장한다.
    2. X 마을에서 각 마을(1, 3, 4)로 돌아가는 경우를 구할 때는 map을 사용하면 X 마을을 시작점으로 각 마을로 돌아가는 거리를 구할 수 있다.
    3. 각 마을(1, 3, 4)에서 X 마을로 가는 경우를 구할 때는 reverseMap을 사용하면 X 마을을 시작점으로 각 마을로 가는 거리를 구할 수 있다.
    4. 이렇게 reverseMap을 사용하면 X 마을이 도착점이 아닌 시작 마을로 해서 간단하게 다익스트라 알고리즘을 2번 돌려주면 원하는 값을 얻을 수 있다.

 

 

 

3. 소스코드


import java.io.*;
import java.util.*;

public class Main {
    static int N, M, X;
    static List<Node>[] map, reverseMap; // 인접 리스트로 저장

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());
        X = Integer.parseInt(st.nextToken());
        map = new ArrayList[N + 1];
        reverseMap = new ArrayList[N + 1];

        for (int i = 1; i <= N; i++) {
            map[i] = new ArrayList<>();
            reverseMap[i] = new ArrayList<>();
        }

        for (int i = 0; i < M; i++) {
            st = new StringTokenizer(br.readLine());
            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());
            int dist = Integer.parseInt(st.nextToken());
            map[start].add(new Node(end, dist));
            reverseMap[end].add(new Node(start, dist));
        }
        int[] go = dijkstra(reverseMap); // 각 노드에서 X로 가는 최단 경로
        int[] back = dijkstra(map); // X에서 각 노드로 가는 최단 경로

        int answer = 0;
        for (int i = 1; i <= N; i++) {
            answer = Math.max(answer, go[i] + back[i]);
        }
        System.out.println(answer);
    }

    public static int[] dijkstra(List<Node>[] inputMap) {
        boolean[] visited = new boolean[N + 1];
        int[] result = new int[N + 1];
        Arrays.fill(result, Integer.MAX_VALUE);
        result[X] = 0; // X에서 X로 가는 경로는 0으로 초기화

        // 다익스트라 알고리즘 시작
        PriorityQueue<Node> queue = new PriorityQueue<>((n1, n2) -> n1.dist - n2.dist);
        queue.add(new Node(X, 0));

        while (!queue.isEmpty()) {
            Node curr = queue.poll();
            if (visited[curr.end]) {
                continue;
            }
            visited[curr.end] = true;
            for (Node node : inputMap[curr.end]) {
                if (curr.dist + node.dist < result[node.end]) {
                    result[node.end] = curr.dist + node.dist;
                }
                queue.add(new Node(node.end, result[node.end]));
            }
        }
        return result;
    }

    static class Node {
        int end, dist;

        public Node (int end, int dist) {
            this.end = end;
            this.dist = dist;
        }
    }
}
  • 저는 다익스트라 알고리즘에 사용될 map과 reverseMap을 인접리스트 형식으로 저장했습니다.
  • PriorityQueue를 사용해서 Node의 dist(거리)를 기준으로 오름차순 정렬되도록 선언했습니다.
  • 거리 기준 오름차순으로 저장하면 해당 노드로 도달할 수 있는 최단 거리를 얻을 수 있고, 같은 노드에 대한 정보가 나중에 들어오면 visited를 통해서 contitnue로 걸러줬습니다.

 

 

 

4. 알게 된 점


  • 그래프에서 어떤 특정 노드까지 이동을 하고, 다시 되돌아와야 하는 경우가 존재한다면, 해당 특정 노드를 시작점으로 해결해 볼 수 있다는 것을 알게 되었습니다. (입력을 거꾸로 저장, reverseMap)

+ Recent posts