머지 소트 트리 (Merge Sort Tree)
정의
머지 소트 트리 (Merge Sort Tree) 는 세그먼트 트리의 각 노드에 해당 구간의 정렬된 부분 배열을 저장하는 자료구조. 구간 [l, r] 내에서 k 이하 원소의 개수, k번째 원소, 특정 범위의 원소 개수 등의 쿼리를 O(log^2 N) 또는 O(log N) 에 처리.
초기 제안은 1990년대 중반 Persistent Segment Tree / Fractional Cascading 연구와 함께 등장. PS에서는 2010년대 초반부터 구간 정렬 쿼리의 정형으로 정착.
문제 상황과 동기
배열 a[0..N-1] 이 주어졌을 때, 다음 쿼리를 Q번 처리:
"구간 [l, r] 에서 x 이하인 원소 개수?"
- naive: 매 쿼리마다 a[l..r] 순회, O(N) × Q = O(NQ). N=Q=10^5면 10^10.
- 전역 정렬: 배열 전체 정렬로는 구간 정보 상실.
- merge sort tree: O(N log N) 빌드 + O(log^2 N) 쿼리.
핵심 통찰: 세그먼트 트리를 만들면서 각 노드마다 자식 두 개를 merge-sort처럼 합치면, 모든 구간의 정렬된 배열을 미리 저장 가능. Binary search로 k 이하 개수를 O(log) 안에 세고, 노드 방문이 O(log N)이니 총 O(log^2 N).
시각화
핵심 아이디어
invariant: 각 노드 tree[i]는 구간 [tree_l, tree_r] 의 원소들을 정렬한 배열. 리프는 길이 1. 내부 노드는 두 자식의 merge.
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] = merge(tree[2*node], tree[2*node+1])
쿼리 (l, r, k): “구간 [l, r] 에서 ≤k 개수”는 세그트리 표준 분할 (O(log N) 노드 방문) + 각 노드에서 upper_bound(k) 호출 (O(log M), M = 노드 구간 길이). 총 O(log N × log N).
공간 복잡도: 각 레벨마다 전체 N개 원소를 중복 저장. O(N log N).
알고리즘
# Build
build(node, l, r):
if l == r:
tree[node] = [a[l]]
return
mid = (l+r)/2
build(2*node, l, mid)
build(2*node+1, mid+1, r)
tree[node] = merge_sorted(tree[2*node], tree[2*node+1])
# Query: count x <= k in [ql, qr]
count_le(node, tl, tr, ql, qr, k):
if ql > tr or qr < tl:
return 0
if ql <= tl and tr <= qr:
return upper_bound(tree[node], k) // k 이하 개수
mid = (tl+tr)/2
return count_le(2*node, tl, mid, ql, qr, k)
+ count_le(2*node+1, mid+1, tr, ql, qr, k)
구현
// Merge Sort Tree: 구간 [l, r] 에서 ≤k 개수 O(log^2 N)
#include <bits/stdc++.h>
using namespace std;
struct MergeSortTree {
int n;
vector<vector<int>> tree;
MergeSortTree(const vector<int>& a) : n(a.size()), tree(4 * n) {
build(1, 0, n - 1, a);
}
void build(int node, int l, int r, const vector<int>& a) {
if (l == r) {
tree[node] = {a[l]};
return;
}
int mid = (l + r) / 2;
build(2 * node, l, mid, a);
build(2 * node + 1, mid + 1, r, a);
merge(tree[2 * node].begin(), tree[2 * node].end(),
tree[2 * node + 1].begin(), tree[2 * node + 1].end(),
back_inserter(tree[node]));
}
// 구간 [ql, qr] 에서 ≤k 개수
int count_le(int node, int tl, int tr, int ql, int qr, int k) {
if (ql > tr || qr < tl) return 0;
if (ql <= tl && tr <= qr) {
return upper_bound(tree[node].begin(), tree[node].end(), k)
- tree[node].begin();
}
int mid = (tl + tr) / 2;
return count_le(2 * node, tl, mid, ql, qr, k)
+ count_le(2 * node + 1, mid + 1, tr, ql, qr, k);
}
int query(int l, int r, int k) {
return count_le(1, 0, n - 1, l, r, k);
}
};
int main() {
int n, q; cin >> n >> q;
vector<int> a(n);
for (auto& v : a) cin >> v;
MergeSortTree mst(a);
while (q--) {
int l, r, k; cin >> l >> r >> k;
cout << mst.query(l - 1, r - 1, k) << "\n";
}
}5 3
1 3 2 5 4
1 3 3
2 5 4
1 5 23
3
2복잡도
| 항목 | 값 |
|---|---|
| 빌드 | O(N log N) 시간 |
| 공간 | O(N log N) |
| 쿼리 (≤k 개수) | O(log^2 N) |
| 쿼리 (k번째) | O(log^3 N) naive / O(log N) (Persistent Segtree) |
k번째 원소 쿼리는 각 노드에서 binary search를 중첩하면 O(log^3 N), Persistent Segment Tree + 좌표 압축으로 O(log N)까지 떨어짐.
변형 / 활용
1. k번째 원소 (k-th smallest)
각 노드에서 “왼쪽 자식에 몇 개 포함되는가”를 세서 binary search on answer 또는 재귀 분할. O(log^3 N).
2. 구간 [a, b] 범위 개수
count_le(r, b) - count_le(r, a-1) 로 O(log^2 N).
3. Persistent Segment Tree 와 결합
좌표 압축 후 Persistent Segtree로 k번째 쿼리 O(log N).
4. 2D range query
2D Merge Sort Tree: 각 노드에 또 다른 1D tree. O(log^3 N) 또는 Fractional Cascading O(log^2 N).
함정
1. upper_bound vs lower_bound
“≤k 개수”는 upper_bound(k) (k보다 큰 첫 원소). lower_bound(k)는 k 이상.
2. 공간 초과
O(N log N) 공간. N=10^6 이면 배열 크기 수천만. vector<int> 대신 압축된 표현 또는 Persistent Segtree.
3. 갱신 불가
기본 Merge Sort Tree는 원소 갱신 시 O(N log N) 재빌드. 갱신이 잦으면 PST / Wavelet Tree 고려.
4. 1-indexed vs 0-indexed
쿼리 입력이 1-indexed면 query(l-1, r-1, k) 호출. 코드 내부는 0-indexed.
BOJ 연습 문제
| 번호 | 제목 | 정답률 | 링크 |
|---|---|---|---|
| BOJ 13537 | 수열과 쿼리 1 | - | kokoa-lab |
| BOJ 7469 | K번째 수 | - | kokoa-lab |
| BOJ 11012 | Egg | - | kokoa-lab |
참고
이 글의 용어 (4개)
- 세그먼트 트리 (Segment Tree)algorithm
- 정의 세그먼트 트리 (Segment Tree) 는 배열의 구간 쿼리 (range query) 와 점 갱신 (point update) 를 모두 O(log N) 에 처리하는 이진 트…
- 이분 탐색 (Binary Search)algorithm
- 정의 이분 탐색 (Binary Search) 은 정렬된 시퀀스에서 목표값의 위치를 O(log N) 에 찾는 알고리즘. 매 단계에서 후보 구간을 절반으로 줄인다. 탐색이 본질이 아…
- Persistent Segment Tree (영속 세그): 시점별 스냅샷algorithm
- 정의 Persistent Segment Tree 는 매 갱신마다 이전 버전을 그대로 유지하면서 새 버전을 만드는 세그먼트 트리입니다. 각 버전은 루트 포인터 로 식별됩니다. 핵심…
- Wavelet Tree: 값 범위 이진 분할algorithm
- 정의 Wavelet Tree 는 시퀀스의 값 범위를 이진 분할하며 만든 트리 자료구조입니다. k-th smallest in range, rank/select, count in r…
💬 댓글