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

재귀 트리 DP / Rerooting

· 수정 · 📖 약 4분 · 1,175자/단어 #algorithm #tree #dp #rerooting
rerooting, all root dp, 트리 재귀 DP, 전체 루트 DP

정의

Rerooting (재귀 트리 DP)모든 정점을 루트로 하는 트리 DP 결과를 O(N) 에 구하는 기법. 두 번의 DFS 로 “루트 v 에서 서브트리 DP” → “루트 v 에서 전체 트리 DP” 를 모든 v 에 대해 계산.

대표 응용: “정점 v 를 루트로 한 트리의 높이”, “v 에서 모든 정점까지 거리 합”, “v 를 루트로 한 서브트리 개수” 등을 모든 v 에 대해 O(N) 에 계산.

문제 상황과 동기

“모든 정점을 루트로 했을 때의 트리 DP 값” 을 구하려면:

  • naive: 각 정점마다 루트로 DFS → O(N^2) → N=10^5 면 불가능.
  • rerooting: DFS 두 번 → O(N). 첫 DFS 로 아래방향 (자식), 두 번째 DFS 로 위방향 (부모) 정보 갱신.

핵심 통찰: 첫 DFS 에서 자식 방향 DP, 두 번째 DFS 에서 부모 정보를 자식에게 전달. 각 정점은 “부모쪽 서브트리” 를 포함한 전체 트리 정보를 받음.

PS / 실무 위치: BOJ 골드~플래티넘 (트리 DP), 네트워크 분석 (모든 노드 중심 메트릭).

시각화

핵심 아이디어

invariant:

  1. DFS 1 (down): 정점 u 의 “아래방향” DP 값 계산. dp_down[u] = f(dp_down[child1], dp_down[child2], ...).
  2. DFS 2 (up): 정점 u 의 “위방향” DP 값 = 부모의 전체 DP 에서 u 서브트리를 제외한 값. dp_up[u] = g(dp_up[parent], dp_down[siblings]).
  3. 최종 답: answer[u] = combine(dp_down[u], dp_up[u]).

알고리즘 흐름:

rerooting(root):
    dfs_down(root, -1)    # 자식 방향 DP
    dfs_up(root, -1, 0)   # 부모 방향 DP
    for u in 1..N:
        answer[u] = combine(dp_down[u], dp_up[u])

알고리즘

dfs_down(u, parent):
    dp_down[u] = base_value
    for v in children(u):
        dfs_down(v, u)
        dp_down[u] = merge(dp_down[u], dp_down[v])

dfs_up(u, parent, parent_contribution):
    dp_up[u] = parent_contribution
    
    # 자식 v 에게 전달할 "형제들 + 부모" 기여도 계산
    prefix = [base], suffix = [base]
    children = [v for v in adj[u] if v != parent]
    
    for i in range(len(children)):
        prefix[i+1] = merge(prefix[i], dp_down[children[i]])
    for i in range(len(children)-1, -1, -1):
        suffix[i] = merge(suffix[i+1], dp_down[children[i]])
    
    for i, v in enumerate(children):
        contribution = merge(dp_up[u], merge(prefix[i], suffix[i+1]))
        dfs_up(v, u, contribution)

prefix/suffix 로 “v 를 제외한 형제들” 의 DP 값을 O(1) 에 계산.

구현

// Rerooting: 모든 루트에 대한 트리 높이, O(N)
#include <bits/stdc++.h>
using namespace std;

vector<int> adj[100005];
int dp_down[100005], dp_up[100005], ans[100005];

void dfs_down(int u, int p) {
  dp_down[u] = 0;
  for (int v : adj[u]) {
      if (v == p) continue;
      dfs_down(v, u);
      dp_down[u] = max(dp_down[u], dp_down[v] + 1);
  }
}

void dfs_up(int u, int p, int parent_val) {
  dp_up[u] = parent_val;
  ans[u] = max(dp_down[u], dp_up[u]);
  
  vector<int> children;
  for (int v : adj[u]) {
      if (v != p) children.push_back(v);
  }
  
  int k = children.size();
  vector<int> prefix(k + 1, 0), suffix(k + 1, 0);
  for (int i = 0; i < k; i++)
      prefix[i+1] = max(prefix[i], dp_down[children[i]] + 1);
  for (int i = k - 1; i >= 0; i--)
      suffix[i] = max(suffix[i+1], dp_down[children[i]] + 1);
  
  for (int i = 0; i < k; i++) {
      int v = children[i];
      int contribution = max({dp_up[u] + 1, prefix[i] + 1, suffix[i+1] + 1});
      dfs_up(v, u, contribution);
  }
}

int main() {
  int n; cin >> n;
  for (int i = 0; i < n - 1; i++) {
      int u, v; cin >> u >> v;
      adj[u].push_back(v);
      adj[v].push_back(u);
  }
  
  dfs_down(1, 0);
  dfs_up(1, 0, 0);
  
  for (int i = 1; i <= n; i++)
      cout << "ans[" << i << "] = " << ans[i] << "\n";
}
stdin
5
1 2
1 3
2 4
2 5
결과
ans[1] = 2
ans[2] = 2
ans[3] = 3
ans[4] = 3
ans[5] = 3

복잡도

항목
시간O(N)
공간O(N)
DFS 횟수2회

응용 패턴

1. 트리의 지름 (모든 루트)

각 정점 v 를 루트로 한 트리의 지름 = max(자식쪽 최대 2개 경로 합, 부모쪽 + 자식쪽 최대 경로).

2. 거리 합

정점 v 에서 모든 정점까지 거리 합. DFS 1 에서 자식 서브트리 거리 합, DFS 2 에서 부모쪽 거리 합 전달.

3. 서브트리 개수

v 를 루트로 한 트리에서 크기 k 이하 서브트리 개수. DFS 1 에서 자식 서브트리 카운팅, DFS 2 에서 부모쪽 서브트리 포함.

변형

Weighted Rerooting

간선 가중치가 있을 때도 동일. DP 값에 가중치 반영.

Multiple DP Values

정점마다 여러 DP 값 (예: 최대, 최소, 합) 을 동시에 계산 가능. prefix/suffix 를 DP 값마다 분리.

함정

1. prefix/suffix 배열 크기

자식이 k 개면 prefix/suffix 는 k+1 크기. 초기값 주의.

2. base value

DP 의 초기값 (merge 의 항등원) 을 정확히 설정. 예: max → 0 또는 -∞, sum → 0.

3. contribution 계산

자식 v 에게 전달할 “형제들 + 부모” 기여도는 merge(dp_up[u], merge(prefix[i], suffix[i+1])). v 자신을 제외.

4. parent 체크

DFS 2 에서 부모를 자식으로 착각하지 않도록 v != p 체크 필수.

BOJ 연습 문제

번호제목정답률링크
BOJ 15681트리와 쿼리-kokoa-lab
BOJ 1167트리의 지름-kokoa-lab
BOJ 2213트리의 독립집합-kokoa-lab
BOJ 1693트리 색칠하기-kokoa-lab

참고

이 글의 용어 (4개)
중심 분해 (Centroid Decomposition)algorithm
정의 중심 분해 (Centroid Decomposition) 는 트리를 중심 으로 재귀적으로 분할하여 O(log N) 깊이의 분해 트리 를 만드는 기법. 각 레벨에서 트리 크기가…
최소 공통 조상 (Lowest Common Ancestor)algorithm
정의 최소 공통 조상 (LCA, Lowest Common Ancestor) 는 트리에서 두 노드 u, v 의 공통 조상 중 가장 깊은 (루트에서 가장 먼) 노드. Binary L…
Graph DP: 트리/DAG 위 DPalgorithm
정의 Graph DP 는 그래프 구조를 이용한 DP. 사이클 없는 그래프 (트리, DAG) 에서 특히 자연스러움. Tree DP 루트 트리에서 자식 서브트리 결과를 조합. Rer…
Heavy-Light Decompositionalgorithm
정의 Heavy-Light Decomposition (HLD) 는 트리를 O(log N) 개의 경로 (chain) 로 분해해 임의 경로 쿼리 (u-v 경로의 합/최대/최소) 를 …

💬 댓글

사이트 검색 / 명령어

검색

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