세그먼트 트리 (Segment Tree)
정의
세그먼트 트리 (Segment Tree) 는 배열의 구간 쿼리 (range query) 와 점 갱신 (point update) 를 모두 O(log N) 에 처리하는 이진 트리 자료구조.
리프 노드가 원소, 내부 노드가 구간의 합/최솟값/최댓값/GCD 등 결합 가능한 연산 (associative operation) 결과를 저장. 높이 ⌈log₂ N⌉ 의 완전 이진 트리.
누적 합은 O(1) 쿼리지만 갱신이 O(N), Fenwick Tree (BIT) 는 합/XOR 만 가능. 세그먼트 트리는 임의 결합 연산 을 O(log N) / O(log N) 로 일반화.
문제 상황과 동기
길이 N 배열에서:
- 쿼리:
sum(l, r),min(l, r),max(l, r)등 Q 개 - 갱신:
a[i] = x업데이트 U 개
| 방식 | 쿼리 | 갱신 | 비고 |
|---|---|---|---|
| naive | O(N) | O(1) | 쿼리마다 순회 |
| [[Prefix Sum | 누적 합]] | O(1) | O(N) |
| Segment Tree | O(log N) | O(log N) | 균형 |
핵심 통찰: 구간을 이진 분할하면 임의 구간이 O(log N) 개 노드의 합집합. 각 노드가 관할 구간의 연산 결과를 캐싱하므로, 경로상 노드만 확인.
PS에서 “구간 합/최소/최대 쿼리 + 업데이트” 가 동시에 나오면 세그먼트 트리 또는 레이지 프로파게이션.
시각화
핵심 아이디어
invariant: 노드 [l, r] 의 값 = 구간 [l, r] 의 연산 결과.
tree[node] = op(tree[2*node], tree[2*node+1])
op 는 결합 법칙 만족 (associative): op(a, op(b, c)) = op(op(a, b), c). 대표 예: +, min, max, gcd, lcm, XOR.
구조:
- 1-indexed 완전 이진 트리.
tree[1]= 전체 구간. - 노드 i 의 왼쪽 자식 = 2i, 오른쪽 자식 = 2i+1.
- 리프 노드 N 개 → 전체 노드 ≤ 4N (보통 4N 배열).
쿼리 [L, R]:
- 현재 노드 [l, r] 이 [L, R] 에 완전히 포함 → 즉시 반환.
- 겹치지 않으면 스킵.
- 부분 겹침 → 왼쪽/오른쪽 재귀, 결과 병합.
갱신 a[idx] = val:
- 리프 노드까지 내려가 val 로 갱신.
- 돌아오며 조상 노드들 모두 재계산.
알고리즘
빌드
build(node, l, r):
if l = r:
tree[node] = a[l]
else:
mid = (l + r) / 2
build(2*node, l, mid)
build(2*node+1, mid+1, r)
tree[node] = op(tree[2*node], tree[2*node+1])
쿼리
query(node, l, r, L, R):
if r < L or R < l: # 범위 벗어남
return identity
if L ≤ l and r ≤ R: # 완전 포함
return tree[node]
mid = (l + r) / 2
left = query(2*node, l, mid, L, R)
right = query(2*node+1, mid+1, r, L, R)
return op(left, right)
점 갱신
update(node, l, r, idx, val):
if l = r: # 리프
tree[node] = val
else:
mid = (l + r) / 2
if idx ≤ mid:
update(2*node, l, mid, idx, val)
else:
update(2*node+1, mid+1, r, idx, val)
tree[node] = op(tree[2*node], tree[2*node+1])
구현
// 구간 합 세그먼트 트리, 1-indexed
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
class SegTree {
vector<ll> tree;
int n;
void build(vector<ll>& a, int node, int l, int r) {
if (l == r) {
tree[node] = a[l];
} else {
int mid = (l + r) / 2;
build(a, 2*node, l, mid);
build(a, 2*node+1, mid+1, r);
tree[node] = tree[2*node] + tree[2*node+1];
}
}
ll query(int node, int l, int r, int L, int R) {
if (r < L || R < l) return 0;
if (L <= l && r <= R) return tree[node];
int mid = (l + r) / 2;
return query(2*node, l, mid, L, R) + query(2*node+1, mid+1, r, L, R);
}
void update(int node, int l, int r, int idx, ll val) {
if (l == r) {
tree[node] = val;
} else {
int mid = (l + r) / 2;
if (idx <= mid) update(2*node, l, mid, idx, val);
else update(2*node+1, mid+1, r, idx, val);
tree[node] = tree[2*node] + tree[2*node+1];
}
}
public:
SegTree(vector<ll>& a) : n(a.size() - 1), tree(4 * n) {
if (n > 0) build(a, 1, 1, n);
}
ll query(int l, int r) { return query(1, 1, n, l, r); }
void update(int idx, ll val) { update(1, 1, n, idx, val); }
};
int main() {
ios::sync_with_stdio(0); cin.tie(0);
int n, q; cin >> n >> q;
vector<ll> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
SegTree seg(a);
while (q--) {
int t; cin >> t;
if (t == 1) {
int idx; ll val; cin >> idx >> val;
seg.update(idx, val);
} else {
int l, r; cin >> l >> r;
cout << seg.query(l, r) << "\n";
}
}
}5 4
1 2 3 4 5
2 1 3
1 2 10
2 1 3
2 2 56
16
22복잡도
| 항목 | 값 |
|---|---|
| 빌드 | O(N) |
| 쿼리 | O(log N) |
| 점 갱신 | O(log N) |
| 공간 | O(N), 실제 4N 배열 |
트리 높이 = ⌈log₂ N⌉, 각 연산이 경로상 노드만 방문.
변형 / 활용
1. Min / Max 세그먼트 트리
// op 를 min 으로 변경
tree[node] = min(tree[2*node], tree[2*node+1]);
identity는 INF (min), -INF (max).
2. GCD / LCM
ll gcd(ll a, ll b) { return b ? gcd(b, a % b) : a; }
tree[node] = gcd(tree[2*node], tree[2*node+1]);
3. 구간 갱신 (Lazy Propagation)
점 갱신이 아니라 [l, r] += k 같은 구간 갱신 이 필요하면 레이지 프로파게이션 추가. O(log N) 유지.
4. 2D 세그먼트 트리
세그먼트 트리의 각 노드가 또 다른 세그먼트 트리. 2D 구간 쿼리/갱신 O(log² N).
5. Persistent Segment Tree
과거 버전 유지. 함수형 자료구조로 O(log N) 공간/시간 추가.
함정
1. 배열 크기
tree[4 * N] 이 안전. 2 * N 으로 하면 일부 케이스 overflow.
2. 1-indexed vs 0-indexed
쿼리 범위 [L, R] 이 1-indexed 인지 0-indexed 인지 일관성 유지. 헷갈리면 버그.
3. identity 원소
합: 0, 곱: 1, min: INF, max: -INF, gcd: 0, XOR: 0. 잘못 설정하면 틀린 답.
4. long long 범위
N=10^5, 각 원소 10^9 이면 합이 10^14. C++ 에서 int 로 하면 오버플로우.
5. 재귀 깊이
Python 은 재귀 한도 (기본 1000). sys.setrecursionlimit(10**6) 필요.
BOJ 연습 문제
| 번호 | 제목 | 정답률 | 링크 |
|---|---|---|---|
| BOJ 2042 | 구간 합 구하기 | 27.3% | kokoa-lab |
| BOJ 10868 | 최솟값 | 35.8% | kokoa-lab |
| BOJ 2357 | 최솟값과 최댓값 | 35.2% | kokoa-lab |
| BOJ 1275 | 커피숍2 | 33.5% | kokoa-lab |
참고
이 글의 용어 (4개)
- 누적 합 (Prefix Sum)algorithm
- 정의 누적 합 (Prefix Sum) 은 배열 에 대해 (또는 1-indexed ) 을 미리 계산해 두고, 임의 구간 합 을 O(1) 에 구하는 정형. 문제 풀이에서 "구간 N …
- 레이지 프로파게이션 (Lazy Propagation)algorithm
- 정의 레이지 프로파게이션 (Lazy Propagation) 은 세그먼트 트리의 확장으로, 구간 갱신 (range update) 와 구간 쿼리 (range query) 를 모두 O…
- 희소 배열 (Sparse Table)algorithm
- 정의 희소 배열 (Sparse Table) 은 정적 배열에서 결합 법칙을 만족하는 idempotent 연산 (min, max, gcd, lcm 등) 의 구간 쿼리를 O(1) 시간…
- Fenwick Tree (Binary Indexed Tree): 구간 합 O(log N)algorithm
- 정의 Fenwick Tree (또는 BIT, Binary Indexed Tree) 는 배열의 prefix sum 을 O(log N) 에 갱신·조회 하는 자료구조입니다. Peter…
이 개념을 다룬 위키 페이지 (25)
- wikiBinary Search Tree (BST): 정렬된 이진 트리
- wiki카테시안 트리 (Cartesian Tree)
- wiki자료구조 (Data Structures)
- wikiDynamic Segment Tree: 지연 노드 생성
- wikiFenwick Tree (Binary Indexed Tree): 구간 합 O(log N)
- wiki레이지 프로파게이션 (Lazy Propagation)
- wiki리차오 트리 (Li Chao Tree)
- wiki머지 소트 트리 (Merge Sort Tree)
- wiki다차원 세그먼트 트리 (Multi-dimensional Segment Tree)
- wikiPersistent Segment Tree (영속 세그): 시점별 스냅샷
- wiki퍼시스턴트 세그먼트 트리 (Persistent Segment Tree)
- wikiRMQ (Range Minimum Query)
- wiki로프 (Rope)
- wiki희소 배열 (Sparse Table)
- wiki덱 DP (슬라이딩 윈도우 최적화)
- wiki차분 배열 (Difference Array)
- wiki누적 합 (Prefix Sum)
- wiki스위핑 (Sweeping)
- wikiCDQ 분할 정복 (CDQ Divide and Conquer)
- wikiMo's Algorithm (Mo's)
- wikiOffline Queries (오프라인 쿼리)
- wiki전처리 (Precomputation)
- wiki제곱근 분할 (Sqrt Decomposition)
- wiki오일러 투어 테크닉 (Euler Tour Technique)
- wikiHeavy-Light Decomposition
💬 댓글