본문 바로가기

코딩테스트/문제

[백준] 31938. 현대모비스 트럭 군집주행 - JAVA

✨ 문제 상세


처음에는 각 도시를 거쳐서 갈 수 있는 다른 도시들의 수를 구한 후, 이를 기반으로 각 노드에 몇 대의 트럭이 필요한지를 결정함으로써 최종 비용을 계산하는 방법을 구상했다.

 

그러나 문제 의도에 비해 풀이 과정이 많이 복잡해지는 기분이 들어 새로운 방법을 시도하게 되었다.

 

🔍문제 풀이

문제의 예시를 참고해보자.

각 노드에 도착하는 트럭이 한 대씩 존재하므로, 여기서도 각 노드에 한 대씩 트럭을 보낸다고 가정하겠다.

 

먼저, 2번노드까지의 최단 경로를 생각해보자.

자신이 첫번째이므로 따라갈 트럭이 없어 10이 된다.

다음으로 3번 노드까지의 최단 경로를 보자.

2번까지는 위의 트럭과 함께 이동하면 되므로 90% 가격에 이동이 가능하다.

반면, 2번에서 3번으로 이동하는 도로는 처음으로 따라갈 트럭이 없으므로 100% 가격을 온전히 지불해야 한다.

  • 1 → 2 : 10 * 0.9 = 9
  • 2 → 3 :  20
  • 총비용 : 9 + 20 = 29

4번 노드까지의 최단 경로 또한 동일하다.

  • 1 → 2 : 10 * 0.9 = 9
  • 2 → 4 :  30
  • 총비용 : 9 + 30 = 39

 

마지막으로 5번 노드까지의 최단 경로를 보자. 이 경우에는 총 2가지 경로를 사용할 수 있다.

  • 1 → 2 → 3 → 5 : (10 * 0.9) + (20 * 0.9) + 30 = 57
    (1에서 3까지의 경로 : 3번의 트럭을 따라가면 비용 절감 가능)
  • 1 → 2 → 4 → 5 : (10 * 0.9) + (30 * 0.9) + 20 = 56
    (1에서 4까지의 경로 : 4번의 트럭을 따라가면 비용 절감 가능)

두 경로 모두 총 거리는 60으로 동일하므로, 최소 비용은 56이 된다.


위의 과정을 통해 아래의 결론을 얻을 수 있다.

  • [ ... →  N →  M ] 의 경로를 통해 M번 노드로 가는 트럭은, 마지막 엣지를 제외하면 반드시 N번 노드로 가는 트럭과 동일한 경로를 사용 (최단이므로)
  • N번 노드까지의 최소 비용을 구할 때, N번 노드로 진입하는 노드( = 마지막 엣지)를 제외하고는 90%로 계산 가능
  • 즉, 총 비용은 1번 노드에서 각 노드까지의 최소 비용을 더한 값이 됨 

다시 말해, 예시의 정답을 구하고 싶다면

  • 1 → 2 : 10 = 10
  • 1 → 3 : (10 * 0.9) + 20 = 29
  • 1 → 4 : (10 * 0.9) + 30 = 39
  • 1 → 5 : (10 * 0.9) + (30 * 0.9) + 20 = 56

⇒ 최종 cost = 10+29+39+56 = 134

와 같은 흐름으로 계산이 가능한 것이다.


❌ 코드 1 : 오답

정답 배열과 다익스트라에 사용되는 PQ에 서로 다른 값을 넣어줌으로써 계산 과정을 최소화하고자 했다.

  • 정답 배열 : 직전 노드까지의 경로 90% + 마지막 경로 100% 저장
  • PQ : (객체) 노드번호 / 해당 노드까지의 총 거리 90%

PQ에서 poll된 노드번호가 N이라 할 때, 저장된 cost는 N번을 지나 다른 노드로 가는 경우, 즉 이미 N번까지의 모든 도로가 사용된 후의 가격을 의미하므로 복잡한 과정 없이 빠른 계산이 가능할 것이라 생각했다.
 
🔽 틀린코드

public class BJ_31938 {
    static final long INF = Long.MAX_VALUE;
    static int N, M;
    static long[] distance, answer;
    static List<List<Node>> graph = new ArrayList<>();
    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());

        for (int i = 0; i <= N; i++) {
            graph.add(new ArrayList<>());
        }

        // 그래프 정보 입력받기
        for (int i = 0; i < M; i++) {
            st = new StringTokenizer(br.readLine());
            int s = Integer.parseInt(st.nextToken());
            int e = Integer.parseInt(st.nextToken());
            int c = Integer.parseInt(st.nextToken());

            graph.get(s).add(new Node(e, c));
            graph.get(e).add(new Node(s, c));
        }

        distance = new long[N+1];
        answer = new long[N+1];
        Arrays.fill(distance, INF);

        dijkstra(1);

        long total = 0L;
        for (int i = 1; i <= N; i++) {
            total += distance[i];
        }
        System.out.println(total);
    }

    public static void dijkstra (int start) {
        PriorityQueue<Node> pq = new PriorityQueue<>(Comparator.comparing(n -> n.cost));
        boolean[] visited = new boolean[N+1];

        pq.offer(new Node(start, 0));
        distance[start] = 0;

        while(!pq.isEmpty()) {
            Node now = pq.poll();

            // 선택 노드 방문 처리
            if(visited[now.idx]) continue;
            else visited[now.idx] = true;

            for (Node next : graph.get(now.idx)) {
                if(!visited[next.idx] && distance[next.idx] >= now.cost + next.cost) {
                    distance[next.idx] = now.cost + next.cost;
                    pq.add(new Node(next.idx, (long) (now.cost + (next.cost * 0.9))));
                }
            }
        }
    }

    public static class Node {
        int idx;
        long cost;

        public Node (int idx, long cost) {
            this.idx = idx;
            this.cost = cost;
        }
    }
}

 
당당하게 제출했지만 1%에서 바로 틀려버렸다....

 

🔍 틀린 이유

위의 코드에서 찾아볼 수 있는 반례는 다음과 같다.

3 3
1 2 1100
1 3 1100
2 3 20

// 정답 : 2200
// WA : 2110

 

문제를 잘 보면 트럭은 "최소 비용"이 아닌, "최단 거리"를 기준으로 이동한다고 적혀있다.

 

반례에서 1 → 3까지의 거리와 비용을 계산해보자.

  • 1 → 3
    - 거리: 1100
    - 비용: 1100

  • 1 → 2 → 3
    - 거리: 1120
    - 비용: (1100 * 0.9) + 20 = 1010

문제의 조건대로라면, 트럭은 최단 거리로 이동하므로 1 → 2 → 3 경로를 사용할 수 없다.

 

그러나 위의 코드에서는 PQ에 전체 거리의 90%, 즉 990을 cost로 넣었기 때문에 기존 최단거리(1100) 보다 새로운 최단 거리(990+20)가 더 짧다고 판단해, 최종 비용을 갱신해버린다.

 

즉, 오차 없이 정확한 계산을 하기 위해서는 결국 총 거리와 총 비용 정보를 따로 저장해야 하는 것이었다... 


✔️ 코드 2 : 정답

distance로는 실제 최단 거리를, answer로는 총 비용을 관리하도록 수정하였다.

이에 따라 PQ에서는 실제 거리를 기준으로 사용할 수 있게 되었다.
 
🔽 전체 코드

public class BJ_31938 {
    static final long INF = Long.MAX_VALUE;
    static int N, M;
    static long[] distance, answer;
    static List<List<Node>> graph = new ArrayList<>();
    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());

        for (int i = 0; i <= N; i++) {
            graph.add(new ArrayList<>());
        }

        // 그래프 정보 입력받기
        for (int i = 0; i < M; i++) {
            st = new StringTokenizer(br.readLine());
            int s = Integer.parseInt(st.nextToken());
            int e = Integer.parseInt(st.nextToken());
            int c = Integer.parseInt(st.nextToken());

            graph.get(s).add(new Node(e, c));
            graph.get(e).add(new Node(s, c));
        }

        distance = new long[N+1];
        answer = new long[N+1];
        Arrays.fill(distance, INF);

        dijkstra(1);

        long total = 0L;
        for (int i = 1; i <= N; i++) {
            total += answer[i];
        }
        System.out.println(total);
    }

    public static void dijkstra (int start) {
        PriorityQueue<Node> pq = new PriorityQueue<>(Comparator.comparing(n -> n.cost));
        boolean[] visited = new boolean[N+1];

        pq.offer(new Node(start, 0));
        distance[start] = 0;

        while(!pq.isEmpty()) {
            Node now = pq.poll();

            // 선택 노드 방문 처리
            if(visited[now.idx]) continue;
            else visited[now.idx] = true;

            for (Node next : graph.get(now.idx)) {
                long realCost = distance[now.idx] + next.cost;

                // 미방문 && 새로운 distance가 최소인 경우(같아도 처리)
                if(visited[next.idx] || distance[next.idx] < realCost) continue;

                // 실제 거리(distance)는 같아도 감소한 거리값은 다를 수도 있음
                // -> 같은 거리인 경우에도 업데이트 (savedCost는 항상 realCost보다 같거나 작으므로)
                answer[next.idx] = (long) (distance[now.idx] * 0.9) + next.cost;

                // 기존 경로(100%) 다익스트라 정보 갱신
                distance[next.idx] = realCost;
                pq.offer(new Node(next.idx, realCost));
            }
        }
    }

    public static class Node {
        int idx;
        long cost;

        public Node (int idx, long cost) {
            this.idx = idx;
            this.cost = cost;
        }
    }
}

 
 
드디어 해결!