ALGORITHM NOTE2

BOJ 14570 - 나무 위의 구슬

설마 트리라서 나무인건 아니지?

#algorithm#boj#gold#tree#recursion
아카이브로 돌아가기

문제 링크

문제

이진 트리란, 위처럼 모든 노드의 자식의 수가 2개 이하인 트리이다.

각 노드에 쓰여 있는 수는 노드의 번호를 의미한다.

특히, 이 문제에서는 루트가 고정되어 있으며, 노드의 순서가 중요한(어떤 서브트리에서도 좌우를 변경할 수 없는) 이진 트리에 대해 다루기로 한다.

이진 트리의 루트에 구슬을 하나 올려놓으면 구슬은 아래와 같은 과정을 거쳐 떨어진다.

  1. 현재 구슬이 놓인 노드의 자식이 없다면 그 자리에서 멈춘다.
  2. 1을 만족하지 않으며, 만일 현재 구슬이 놓인 노드의 자식 노드가 한 개라면 해당 자식 노드로 떨어진다.
  3. 1, 2를 만족하지 않으며, 만일 현재 구슬이 놓인 노드의 자식 노드가 두 개라면, 현재 노드의 왼쪽 서브트리에 담긴 모든 구슬의 수가 오른쪽 서브트리에 담긴 모든 구슬의 수보다 작거나 같을 경우 왼쪽 자식 노드로 떨어진다. 그 외의 경우에는 오른쪽 자식 노드로 떨어진다.

1~3번의 조건을 다시 체크하고 되풀이한다.

구슬은 위와 같은 과정을 거쳐 결국 단말 노드에 쌓이게 된다.

예를 들어, 위의 그림과 같은 트리에 구슬을 떨어뜨릴 경우,

첫 다섯 개의 구슬은 2번, 4번, 2번, 5번, 2번 노드에 차례대로 떨어지게 된다.

위처럼 트리가 충분히 작거나 구슬의 수가 충분히 적을 경우엔 직접 시뮬레이션을 통해

구슬이 떨어지는 순서를 유추할 수가 있다.

하지만, 우리가 관심있는 것은 큰 트리에서 많은 수의 구슬을 떨어뜨리는 과정이다.

임의의 이진 트리가 주어지고, K가 주어졌을 때

K번째 구슬이 어느 노드에서 멈추게 될 지 충분히 빠르게 계산해낼 수 있을까?

입력

첫 줄에 이진 트리의 노드의 수 N이 주어진다. (1 <= N <= 200000)

둘째 줄부터 N개의 줄에 걸쳐, U V가 주어진다.

i번째 줄에 주어지는 U, V는 각각 i번 노드의 왼쪽 자식이 U, 오른쪽 자식이 V임을 의미한다.

만약 U = -1 또는 V = -1이라면, 해당 위치에 자식 노드가 존재하지 않는다는 것이다.

그 외의 경우엔 항상 2 <= U, V <= N을 만족한다.

이어 마지막 줄에 문제에서 설명한 K가 주어진다. (1 <= K <= 10^18)

주어지는 트리는 항상 올바른 이진 트리임이 보장되며, 루트는 항상 1번 노드이다.

출력

K번째 구슬이 떨어지는 노드의 번호를 출력한다.

풀이

직접 K개의 구슬을 시뮬레이션하면 당연히 불가능하다. 핵심은 K번째 구슬 하나만 볼 때, 현재 노드에서 어느 자식으로 가는지가 결국 K의 홀짝으로 결정된다는 점이다.

양쪽 자식이 모두 있는 노드에서는 구슬이 번갈아 비슷하게 분배된다. 첫 번째, 세 번째, 다섯 번째 구슬은 왼쪽으로 가고, 두 번째, 네 번째, 여섯 번째 구슬은 오른쪽으로 간다. 그래서 현재 노드에서 K가 홀수면 왼쪽으로, 짝수면 오른쪽으로 내려가면 된다.

그리고 한쪽으로 내려간 뒤 그 서브트리 안에서의 "몇 번째 구슬인가"도 다시 계산할 수 있다. 홀수면 (K + 1) / 2, 짝수면 K / 2가 된다. 자식이 하나뿐이면 비교할 필요 없이 그쪽으로 그대로 내려가면 된다.

코드

java
import java.io.*;
import java.util.*;
 
public class Main {
 
    static int N;
    static int[][] graph;
    static int[] leaf;
 
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
 
        N = Integer.parseInt(br.readLine());
        graph = new int[N + 1][2];
        leaf = new int[N + 1];
 
        for (int i = 1; i <= N; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int U = Integer.parseInt(st.nextToken());
            int V = Integer.parseInt(st.nextToken());
 
            graph[i][0] = U;
            graph[i][1] = V;
        }
 
        long K = Long.parseLong(br.readLine());
        int ans = dfs(1, K);
        System.out.println(ans);
    }
 
    static int dfs(int n, long K) {
        if (graph[n][0] == -1) {
            if (graph[n][1] == -1) return n;    // 자식 없음
            else return dfs(graph[n][1], K);    // 오른쪽 자식만 있음
        }
        else if (graph[n][1] == -1) return dfs(graph[n][0], K); // 왼쪽 자식만 있음
        else if (K % 2 == 1) return dfs(graph[n][0], (K + 1) / 2);  // 양쪽 자식 있음 - 좌측으로 (K 홀수)
        return dfs(graph[n][1], K / 2); // 양쪽 자식 있음 - 우측으로 (K 짝수)
    }
}

복잡도

  • 시간 복잡도: 트리 높이를 HH라 하면 K번째 구슬의 경로만 따라가므로 O(H)O(H)이다.
  • 공간 복잡도: 트리 배열과 재귀 스택을 사용하므로 O(N)O(N)이다.

마무리

많은 구슬이 떨어지는 과정처럼 보여도, 실제로는 K번째 구슬 하나의 경로만 추적하면 된다. 그 경로를 결정하는 정보가 서브트리 크기가 아니라 K의 홀짝이라는 점이 이 문제의 핵심이다.