[백준] 15681: 트리와 쿼리 - JAVA[자바]

2025. 10. 10. 16:12·PS/BAEKJOON

안녕하세요.

오늘은 [백준] 15681: 트리와 쿼리 문제를 풀며 TLE을 경험했습니다...

이번 글에서는 정답코드와 시간초과 코드의 시간복잡도를 분석하고 왜 최적화가 필요했는지 알아보겠습니다.

 

 

🤔 고민했던 점 & 어려웠던 부분

처음 접근 방법은 다음과 같았습니다.

1. 문제에서 정해진 루트(R)을 기준으로 DFS 탐색하여 부모 정보를 기록한다.

2. 쿼리마다 DFS 탐색하여 서브트리의 크기를 계산한다.

// hyeblee
import java.util.*;
import java.io.*;


public class Main {
    
    // 정점 n, 루트 r, 쿼리 수 q
    // 간선 정보 (n-1개) -> 트리니까 단방향으로 연결해야함.
    // 쿼리의 루트노트 q개

    public static List<Integer>[] adjList;
    public static boolean[] visited;
    public static int[] parent;
    public static int n, r, q;
    public static int result = 0;

    // start를 루트로 하는 서브트리의 정점 개수를 구한다.
    public static void dfs(int start) {
        result++;
        visited[start] = true;

        for(int next: adjList[start]) {
            // 인접한 노드가 부모이거나 방문한 적 있으면 탐색하지 않는다.
            if (next == parent[start] || visited[next]) {
                continue;
            }
            dfs(next);
        }
    }

    // r을 루트로 하는 트리의 정점별 부모를 기록한다.
    public static void writeParent(int cur) {
        visited[cur] = true;
        for(int next: adjList[cur]) {
            if (visited[next]) {
                continue;
            }
            parent[next] = cur;
            writeParent(next);
        }
    }


    

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

        n = Integer.parseInt(st.nextToken());
        r = Integer.parseInt(st.nextToken());
        q = Integer.parseInt(st.nextToken());
        
        adjList = new ArrayList[n+1];
        for(int i=0;i<n+1;i++) {
            adjList[i] = new ArrayList<>();
        }
        visited = new boolean[n+1];
        parent = new int[n+1];
       

        // 간선 연결하기
        for(int i=0;i<n-1;i++) {
            st = new StringTokenizer(br.readLine());
            int u = Integer.parseInt(st.nextToken());
            int v = Integer.parseInt(st.nextToken());
            adjList[u].add(v);
            adjList[v].add(u);
        }

        // r을 루트로 하는 트리에서, 각 노드의 정점 기록하기
        parent[r] = r;
        writeParent(r);

        // 쿼리 실행하며 결과 출력하기
        for(int i=0;i<q;i++) {
            int u = Integer.parseInt(br.readLine());

            result = 0;
            Arrays.fill(visited, false);
            dfs(u);
            sb.append(result + "\n");
        }

        System.out.println(sb);
    
    }
}

💥 왜 시간 초과가 발생했을까?

위 코드에서는 쿼리마다 다음과 같은 과정을 반복하고 있습니다.

dfs(u)를 호출하여, 노드 u를 루트로 하는 서브트리의 정점 개수를 매번 새로 계산한다.

 

문제는 이 dfs(u)의 시간복잡도가 O(N) 이라는 점입니다.
트리에서 DFS는 모든 노드를 한 번씩 방문하기 때문이죠.

그런데 이 DFS가 쿼리의 개수 Q만큼 반복되므로, 전체 시간복잡도는 다음과 같습니다:

O(N × Q)

 

⏱️실제 입력 범위로 시간복잡도 분석

  • 정점의 수 N ≤ 10⁵
  • 쿼리의 수 Q ≤ 10⁵

따라서

O(N × Q) = O(10⁵ × 10⁵) = O(10¹⁰)

 

이는 약 100억 번의 연산으로,
보통 1초에 1억 번 연산이 가능하다고 가정했을 때
약 100초가 걸립니다.

즉, 문제의 시간 제한(1초)을 한참 초과하게 됩니다. 🚨

 

✅ 결론

따라서 매 쿼리마다 DFS를 반복하는 방식은 비효율적입니다.
한 번의 DFS로 모든 노드의 서브트리 크기를 미리 계산해두고,
이후 쿼리마다 O(1) 로 결과를 조회하도록 최적화해야 합니다. (메모이제이션 사용)

 

 

 

📌 올바른 접근

1. 문제에서 정해진 루트(R)을 기준으로 DFS 탐색하여 부모 정보를 기록한다.

2. 한 번의 DFS 탐색(O(n))으로 모든 노드의 서브트리 크기를 계산하고 배열에 저장한다.

3. 쿼리마다 O(1)로 배열을 조회하여 서브트리의 크기를 출력한다. 

 

 

⏱️실제 입력 범위로 시간복잡도 분석

  • 부모 노드를 기록하는 DFS: O(N)
  • 서브트리 크기를 계산하는 DFS: O(N)
  • Q개의 쿼리 처리: O(Q)

따라서 전체 시간 복잡도는

O(N+N+Q)=O(2N+Q)

 

최대 입력 조건인 N=10^5, Q=10^5 를 대입하면,

O(2×10^5+10^5) = O(3×10^5)

 

➡️ 약 30만 번의 연산으로,
일반적으로 1억(10⁸) 연산 ≒ 1초 기준을 고려하면 충분히 1초 이내에 수행됩니다.

 

 

 

 

💻 올바른 풀이

// hyeblee
import java.util.*;
import java.io.*;


public class Main {
    
    // 정점 n, 루트 r, 쿼리 수 q
    // 간선 정보 (n-1개) -> 트리니까 단방향으로 연결해야함.
    // 쿼리의 루트노트 q개

    public static List<Integer>[] adjList;
    public static boolean[] visited;
    public static int[] parent;
    public static int[] subtreeCnt;
    public static int n, r, q;

    // r이 루트인 트리에서 모든 서브트리의 크기를 구한다.
    public static int dfs(int start) {
        int cnt = 1; // 자기 자신
        visited[start] = true;

        for(int next: adjList[start]) {
            // 인접한 노드가 부모이거나 방문한 적 있으면 탐색하지 않는다.
            if (next == parent[start] || visited[next]) {
                continue;
            }
            cnt += dfs(next);
        }
        subtreeCnt[start] = cnt;
        return cnt;
    }

    // r을 루트로 하는 트리의 노드별 부모를 기록한다.
    public static void writeParent(int cur) {
        visited[cur] = true;
        for(int next: adjList[cur]) {
            if (visited[next]) {
                continue;
            }
            parent[next] = cur;
            writeParent(next);
        }
    }


    

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

        n = Integer.parseInt(st.nextToken());
        r = Integer.parseInt(st.nextToken());
        q = Integer.parseInt(st.nextToken());
        
        adjList = new ArrayList[n+1];
        for(int i=0;i<n+1;i++) {
            adjList[i] = new ArrayList<>();
        }
        visited = new boolean[n+1];
        parent = new int[n+1];
        subtreeCnt = new int[n+1];
       

        // 간선 연결하기
        for(int i=0;i<n-1;i++) {
            st = new StringTokenizer(br.readLine());
            int u = Integer.parseInt(st.nextToken());
            int v = Integer.parseInt(st.nextToken());
            adjList[u].add(v);
            adjList[v].add(u);
        }

        // r을 루트로 하는 트리에서, 각 노드의 부모 기록하기
        parent[r] = r;
        writeParent(r);
        
        // 한 번의 dfs로 모든 서브트리의 크기 구하기
        Arrays.fill(visited, false);
        dfs(r);

        // 쿼리 실행하며 결과 출력하기
        for(int i=0;i<q;i++) {
            int u = Integer.parseInt(br.readLine());
            int result = subtreeCnt[u];
            sb.append(result + "\n");
        }

        System.out.println(sb);
    
    }
}

'PS > BAEKJOON' 카테고리의 다른 글

[백준] 15693번: 거짓말 - JAVA[자바]  (0) 2025.10.16
[백준] 14502: 연구소 - JAVA [자바]  (0) 2025.09.17
[백준] 14889번 : 스타트와 링크 - JAVA [자바]  (0) 2025.07.01
[백준] 1916 : 최소비용 구하기 - JAVA [자바]  (0) 2025.06.19
[백준] 9465 : 스티커 - JAVA [자바]  (1) 2025.06.18
'PS/BAEKJOON' 카테고리의 다른 글
  • [백준] 15693번: 거짓말 - JAVA[자바]
  • [백준] 14502: 연구소 - JAVA [자바]
  • [백준] 14889번 : 스타트와 링크 - JAVA [자바]
  • [백준] 1916 : 최소비용 구하기 - JAVA [자바]
hyeblee
hyeblee
감자감자
  • hyeblee
    hyeblee
    hyeblee
  • 전체
    오늘
    어제
    • 분류 전체보기
      • PS
        • Programmers
        • BAEKJOON
        • CODETREE
      • ALGORITHM
      • JAVA
      • CS
        • 면접을 위한 CS전공지식
      • SPRING
      • 회고
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃허브
  • 공지사항

  • 인기 글

  • 태그

    java
    백트래킹
    자바
    탐색
    backjoon
    BOJ
    플레이데이터 백엔드 부트캠프
    플레이데이터 백엔드 부트캠프 후기
    플레이데이터 백엔드 후기
    하한값
    구간 칠하기
    왔다 갔던 구역2
    상한값
    날짜와 시간 계산
    spring #springboot #스프링 #스프링부트
    15652
    백준
    arrays.sort #collections.sort #list.sort #객체정렬 #배열정렬 #timsort #dual pivot quicksort #정렬 #자바
    java #deque #자바 #덱
    16954
    흐른 시간 계산
    dfs
    숨바꼭질3
    spring #스프링 #스프링부트 #springboot #please sign in
    java #스프링부트 #자바버전 #자바 버전 충돌 #jvm
    반닫힌 구간
    알고리즘
    플레이데이터 백엔드
    흐른 일수 계산
    BFS
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
hyeblee
[백준] 15681: 트리와 쿼리 - JAVA[자바]
상단으로

티스토리툴바