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

머지 소트 트리 (Merge Sort Tree)

· 수정 · 📖 약 4분 · 1,198자/단어 #algorithm #data-structure #segment-tree #merge-sort-tree
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";
  }
}
stdin
5 3
1 3 2 5 4
1 3 3
2 5 4
1 5 2
결과
3
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 7469K번째 수-kokoa-lab
BOJ 11012Egg-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…

이 개념을 다룬 위키 페이지 (1)

💬 댓글

사이트 검색 / 명령어

검색

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