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

트리 경로 합 (Path Sum on Tree)

· 수정 · 📖 약 4분 · 1,291자/단어 #algorithm #tree #path-query #lca #prefix-sum
Path Sum on Tree, Tree Path Sum, 트리 경로 합, 트리 경로 쿼리, path query on tree, root-to-node sum

정의

트리 경로 합 (Path Sum on Tree) 는 트리에서 두 정점 사이의 유일 경로 위 정점 (또는 간선) 값의 합 을 구하는 문제입니다.

문제 상황은 크게 두 가지.

  • 정적 (static): 값은 고정. 쿼리 여러 번.
  • 동적 (dynamic): 값 갱신 + 경로 합 쿼리 혼합.

왜 어려운가

Naive: 의 LCA 까지 각각 올라가며 값을 더함. O(depth) per query. 최악 O(N).

이면 O(NQ) = 10^{10} 로 안 됨.

핵심 통찰: 트리에는 유일 경로가 있으므로 다음 관계를 이용:

여기서 는 루트에서 까지의 경로 합. 즉 “루트 기준 접두합 (prefix sum on root-path)”.

시각화

flowchart TD
  R((루트))
  L((LCA))
  U((u))
  V((v))
  R --- L
  L --- U
  L --- V
  L -.->|"두 번 세어짐"| L
  • 구간이 두 번 세어짐.
  • 을 빼면 이 제거.
  • 하지만 자체가 원래 경로 위 정점이므로 은 살려야 함. 따라서 + val[L].

정확히:

간선 값 경로 합

값이 간선 에 있으면 보정이 다릅니다.

를 간선 의 값이라 하고 를 루트에서 까지의 간선 합이라 하면:

간선 값은 정점 값과 달리 LCA 자체가 경로 안에 포함되지 않으므로 + val[L] 보정이 없습니다.

변환 트릭: 간선 의 값을 자식 의 정점 값으로 옮겨 저장하면 정점 값 문제로 통일 가능.

정적 알고리즘 (변경 없음)

전처리 (DFS)

dfs(u, parent):
    S[u] = S[parent] + val[u]
    for v in adj[u]:
        if v != parent:
            dfs(v, u)

쿼리

pathSum(u, v):
    L = lca(u, v)
    return S[u] + S[v] - 2 * S[L] + val[L]
  • 전처리: O(N) DFS + O(N log N) Binary Lifting for LCA
  • 쿼리: O(log N) (LCA)
  • 전체: O((N + Q) log N)

구현 (정적)

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005, LOG = 20;
int up[MAXN][LOG];
int depth[MAXN];
long long S[MAXN];
long long val[MAXN];
vector<int> adj[MAXN];

void dfs(int u, int p) {
  up[u][0] = p;
  for (int i = 1; i < LOG; i++) {
      up[u][i] = up[u][i-1] < 0 ? -1 : up[up[u][i-1]][i-1];
  }
  for (int v : adj[u]) if (v != p) {
      depth[v] = depth[u] + 1;
      S[v] = S[u] + val[v];
      dfs(v, u);
  }
}

int lca(int u, int v) {
  if (depth[u] < depth[v]) swap(u, v);
  int diff = depth[u] - depth[v];
  for (int i = 0; i < LOG; i++) {
      if (diff & (1 << i)) u = up[u][i];
  }
  if (u == v) return u;
  for (int i = LOG - 1; i >= 0; i--) {
      if (up[u][i] != up[v][i]) {
          u = up[u][i]; v = up[v][i];
      }
  }
  return up[u][0];
}

long long pathSum(int u, int v) {
  int L = lca(u, v);
  return S[u] + S[v] - 2 * S[L] + val[L];
}

int main() {
  int n, q;
  cin >> n >> q;
  for (int i = 0; i < n; i++) cin >> val[i];
  for (int i = 0; i < n - 1; i++) {
      int a, b;
      cin >> a >> b;
      a--; b--;
      adj[a].push_back(b); adj[b].push_back(a);
  }
  S[0] = val[0];
  dfs(0, -1);
  while (q--) {
      int u, v;
      cin >> u >> v;
      cout << pathSum(u-1, v-1) << "\n";
  }
}
stdin
5 3
1 2 3 4 5
1 2
1 3
2 4
2 5
4 5
4 3
3 5
결과
11
10
11

동적 알고리즘 (값 변경 지원)

정적 접근은 S[u] 를 미리 계산해두므로 값이 변하면 전체 재계산 필요. 동적 지원에는 다음 옵션:

옵션 A: Euler Tour + Fenwick

Euler Tour 로 트리를 배열로 펼치고, Fenwick 로 구간 합.

in-time, out-time 을 이용해 정점 값을 두 번 (in 에 +val, out 에 -val) 넣으면:

  • Update: O(log N)
  • Query (path sum): 2 * LCA + O(log N)

옵션 B: Heavy-Light Decomposition (HLD)

경로를 O(log N) 개의 chain 으로 나누어 각 chain 에 Segment Tree.

  • Update: O(log N)
  • Query: O(log^2 N)

강력하지만 구현 복잡.

트리 구조 자체도 동적 변경 지원. 매우 복잡.

시각화: LCA 를 통한 경로 합 공식

루트 R
 \
  L (LCA)
 / \
A   B
|   |
u   v

pathSum(u, v) = pathSum(u -> L) + pathSum(L -> v) - val[L]
              = (S[u] - S[L] + val[L]) + (S[v] - S[L] + val[L]) - val[L]
              = S[u] + S[v] - 2*S[L] + val[L]

확장

경로 위 최댓값 / 최솟값

합 대신 max/min. Idempotent 연산이라 Sparse Table 로 O(1) 쿼리 가능:

maxOnPath(u, v):
    L = lca(u, v)
    return max(maxToRoot(u, L), maxToRoot(v, L), val[L])

maxToRoot 는 binary lifting 확장 (max 도 함께 저장).

경로 위 XOR

XOR 은 자기 역원이라 xor(a, a) = 0. LCA 방식이 더 깔끔:

이 XOR 에서는 항상 0.

경로 위 곱 (mod)

모듈러 곱셈. mod 소수면 modular inverse 로 나눗셈 가능:

함정

1. 정점 값 vs 간선 값 보정

  • 정점: + val[L]
  • 간선: 보정 없음

혼동하면 오답. 문제 정의를 명확히.

2. 배열 오버플로

정점 값이 큰 경우 (10^9) 총합은 N * 10^9 = 10^14int64 필수.

3. 자기 자신 경로

pathSum(u, u) = val[u] (LCA = u, S[u] + S[u] - 2*S[u] + val[u] = val[u]).

4. 여러 컴포넌트

숲 (forest) 인 경우 각 트리별 처리. lca 가 다른 트리면 정의 없음.

5. LCA 를 안 쓰고 ETT 만 로 경로 합?

ETT 는 서브트리 합만 자연스럽게. 경로 합은 LCA + prefix trick 이 표준. 두 정점의 in-time 만으로 경로 합은 못 함.

BOJ 연습 문제

번호제목링크
BOJ 13511트리와 쿼리 2BOJ
BOJ 1761정점들의 거리BOJ
BOJ 3176도로 네트워크BOJ
BOJ 1626두 번째로 작은 스패닝 트리BOJ

참고

이 글의 용어 (8개)
세그먼트 트리 (Segment Tree)algorithm
정의 세그먼트 트리 (Segment Tree) 는 배열의 구간 쿼리 (range query) 와 점 갱신 (point update) 를 모두 O(log N) 에 처리하는 이진 트…
오일러 투어 테크닉 (Euler Tour Technique)algorithm
정의 오일러 투어 테크닉 (ETT, Euler Tour Technique) 은 트리를 DFS 방문 순서로 펼쳐 서브트리 쿼리를 구간 쿼리로 변환하는 정형. 각 노드 u 의 in-…
최소 공통 조상 (Lowest Common Ancestor)algorithm
정의 최소 공통 조상 (LCA, Lowest Common Ancestor) 는 트리에서 두 노드 u, v 의 공통 조상 중 가장 깊은 (루트에서 가장 먼) 노드. Binary L…
희소 배열 (Sparse Table)algorithm
정의 희소 배열 (Sparse Table) 은 정적 배열에서 결합 법칙을 만족하는 idempotent 연산 (min, max, gcd, lcm 등) 의 구간 쿼리를 O(1) 시간…
Binary Lifting: 2의 지수 doublingalgorithm
정의 Binary Lifting (이진 상승, doubling) 은 트리 (또는 functional graph) 에서 정점의 $2^k$ 번째 조상 을 미리 계산해 두어 임의의 $…
Fenwick Tree (Binary Indexed Tree): 구간 합 O(log N)algorithm
정의 Fenwick Tree (또는 BIT, Binary Indexed Tree) 는 배열의 prefix sum 을 O(log N) 에 갱신·조회 하는 자료구조입니다. Peter…
Heavy-Light Decompositionalgorithm
정의 Heavy-Light Decomposition (HLD) 는 트리를 O(log N) 개의 경로 (chain) 로 분해해 임의 경로 쿼리 (u-v 경로의 합/최대/최소) 를 …
K번째 조상 (K-th Ancestor)algorithm
정의 K번째 조상 문제 (K-th ancestor) 는 루트 트리에서 정점 $u$ 의 조상 중 $u$ 로부터 정확히 $k$ 만큼 위에 있는 정점 (즉, $u$ 의 $k$ 번째 부…

💬 댓글

사이트 검색 / 명령어

검색

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