본문으로 건너뛰기
김신건의 로그

트리 (Trees)

· 수정 · 📖 약 3분 · 1,198자/단어 #algorithm #tree #graph #dfs
tree, 트리, rooted tree, 루트 트리

정의

트리 (Tree)사이클이 없는 연결 그래프 (acyclic connected graph). N 개 정점이면 정확히 N-1 개 간선. 임의의 두 정점 사이에 유일한 경로 존재.

루트 트리 (Rooted Tree): 특정 정점을 루트로 지정하여 부모-자식 관계를 정의. 대부분 PS 에서 트리는 루트 트리.

문제 상황과 동기

그래프 문제 중 “연결 + 사이클 없음” 조건이 있으면 트리. 트리는 다음 특성으로 많은 문제가 O(N) 으로 풀림:

  • 계층 구조: 루트 → 리프 방향 DFS/BFS 로 순회.
  • 부모-자식 관계: 서브트리 단위로 분할 정복.
  • 경로 유일성: 두 정점 사이 거리 = 경로 길이 (유일).

일반 그래프는 사이클 처리 + 방문 체크가 복잡하지만, 트리는 DFS 한 번이면 모든 정점/간선을 정확히 한 번씩 방문.

시각화

핵심 아이디어

트리 정의 (동치 조건)

N 개 정점 그래프에서 다음은 모두 동치:

  1. 연결 + 사이클 없음
  2. 연결 + 간선 N-1 개
  3. 사이클 없음 + 간선 N-1 개
  4. 임의의 두 정점 사이 경로가 정확히 하나

루트 설정

무방향 트리에서 루트를 정하면 간선에 방향 (부모 → 자식) 이 생긴다. DFS/BFS 로 한 번 순회하며:

  • parent[v]: v 의 부모 (루트는 -1)
  • depth[v]: 루트에서 v 까지 거리
  • subtree_size[v]: v 를 루트로 하는 서브트리 크기

재귀 구조

트리 문제는 서브트리 단위 분할 정복 이 핵심. f(v) = “v 를 루트로 하는 서브트리의 답” 을 자식들로부터 모아서 계산.

알고리즘

Input: 무방향 트리 (N 정점, N-1 간선), 루트 r

dfs(u, par):
    parent[u] = par
    depth[u] = (par == -1 ? 0 : depth[par] + 1)
    subtree_size[u] = 1
    for each child v of u (v != par):
        dfs(v, u)
        subtree_size[u] += subtree_size[v]

main:
    dfs(r, -1)

구현

// 트리 DFS: parent, depth, subtree size 계산
#include <bits/stdc++.h>
using namespace std;

int n;
vector<int> adj[100005];
int parent[100005], depth[100005], sz[100005];

void dfs(int u, int p) {
  parent[u] = p;
  depth[u] = (p == -1 ? 0 : depth[p] + 1);
  sz[u] = 1;
  for (int v : adj[u]) {
      if (v == p) continue;
      dfs(v, u);
      sz[u] += sz[v];
  }
}

int main() {
  ios::sync_with_stdio(0); cin.tie(0);
  cin >> n;
  for (int i = 0; i < n - 1; i++) {
      int u, v; cin >> u >> v;
      u--; v--;
      adj[u].push_back(v);
      adj[v].push_back(u);
  }
  int root = 0;
  dfs(root, -1);
  for (int i = 0; i < n; i++) {
      cout << "v=" << i << " parent=" << parent[i]
           << " depth=" << depth[i]
           << " sz=" << sz[i] << "\n";
  }
}
stdin
5
1 2
1 3
2 4
2 5
결과
v=0 parent=-1 depth=0 sz=5
v=1 parent=0 depth=1 sz=3
v=2 parent=0 depth=1 sz=1
v=3 parent=1 depth=2 sz=1
v=4 parent=1 depth=2 sz=1

복잡도

항목
DFS 순회O(N) 시간 (정점 + 간선 각 1회)
공간O(N) (adj list + 배열)
재귀 깊이O(N) 최악 (일자 트리)

트리는 간선 수가 N-1 로 고정이므로 DFS/BFS 는 항상 O(N).

기본 연산

LCA (Lowest Common Ancestor)

두 정점 u, v 의 최소 공통 조상. naive O(N), binary lifting / HLD 로 O(log N).

트리 지름 (Diameter)

가장 먼 두 정점 사이 거리. DFS 2회로 O(N).

  1. 임의 정점에서 가장 먼 정점 u 찾기
  2. u 에서 가장 먼 정점 v 찾기 → dist(u, v) 가 지름

중심 (Centroid)

제거했을 때 남은 서브트리 크기가 모두 N/2 이하인 정점. 항상 존재 (최대 2개).

변형 / 활용

이진 트리 (Binary Tree)

각 정점의 자식이 최대 2개. 완전 이진 트리는 배열로 표현 (heap).

트라이 (Trie)

문자열 집합을 트리로 표현. 각 간선에 문자 레이블.

세그먼트 트리 / 펜윅 트리

배열을 트리로 표현하여 구간 쿼리 O(log N).

HLD (Heavy-Light Decomposition)

트리 경로 쿼리를 세그트리로 O(log^2 N).

함정

1. 무방향 간선 → 부모 체크

트리 입력은 보통 무방향. DFS 에서 v == parent 체크 빼먹으면 무한 재귀.

2. 루트가 0 이 아닐 때

문제에서 루트를 명시하거나 1번 정점이 루트인 경우 많음. 0-indexed / 1-indexed 주의.

3. 재귀 깊이

Python 은 기본 재귀 깊이 1000. sys.setrecursionlimit() 필수. C++ / Java 는 보통 괜찮지만 일자 트리 (N=10^6) 면 스택 오버플로우 가능.

4. 트리 vs 포레스트

연결 보장 안 되면 포레스트 (여러 트리). 각 컴포넌트마다 DFS.

BOJ 연습 문제

번호제목정답률링크
BOJ 11725트리의 부모 찾기-kokoa-lab
BOJ 1167트리의 지름-kokoa-lab
BOJ 1991트리 순회-kokoa-lab
BOJ 15681트리와 쿼리-kokoa-lab

참고

이 글의 용어 (5개)
깊이 우선 탐색 (DFS)algorithm
정의 깊이 우선 탐색 (Depth-First Search, DFS) 는 그래프 G=(V, E) 에서 갈 수 있는 만큼 깊이 들어가다가 막히면 백트래킹하는 알고리즘. 스택 (LIF…
너비 우선 탐색 (BFS)algorithm
정의 너비 우선 탐색 (Breadth-First Search, BFS) 는 그래프 G=(V, E) 에서 시작 정점 s 로부터 가까운 정점부터 순서대로 방문하는 알고리즘. 큐 (F…
최소 공통 조상 (Lowest Common Ancestor)algorithm
정의 최소 공통 조상 (LCA, Lowest Common Ancestor) 는 트리에서 두 노드 u, v 의 공통 조상 중 가장 깊은 (루트에서 가장 먼) 노드. Binary L…
graphdata-structure
개요
Graph DP: 트리/DAG 위 DPalgorithm
정의 Graph DP 는 그래프 구조를 이용한 DP. 사이클 없는 그래프 (트리, DAG) 에서 특히 자연스러움. Tree DP 루트 트리에서 자식 서브트리 결과를 조합. Rer…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기