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

Wavelet Tree: 값 범위 이진 분할

· 수정 · 📖 약 4분 · 1,396자/단어 #algorithm #data-structure #wavelet
Wavelet Tree, wavelet-tree, 웨이블릿 트리

정의

Wavelet Tree 는 시퀀스의 값 범위를 이진 분할하며 만든 트리 자료구조입니다. k-th smallest in range, rank/select, count in range 등을 O(log |Σ|) 에 답합니다.

  • |Σ| = 값 범위 (좌표 압축 후 N 이하로 만들 수 있음)
  • N = 시퀀스 길이
  • 공간: O(N log |Σ|)
  • 전처리: O(N log |Σ|)
  • 쿼리: O(log |Σ|)

Persistent Segment Tree 와 동일한 쿼리를 지원하면서 구현이 단순하고 공간 효율이 좋습니다.

문제 상황과 동기

배열 A[0..N-1] 에서 다음 쿼리를 처리해야 하는 상황:

  • kth(l, r, k): A[l..r] 에서 k 번째로 작은 원소
  • rank(l, r, v): A[l..r] 에서 v 보다 작은 원소 개수
  • count(l, r, lo, hi): A[l..r] 에서 값이 [lo, hi] 인 원소 개수
방법전처리쿼리공간
정렬 매번O(1)O(N log N)O(1)
Merge Sort TreeO(N log N)O(log² N)O(N log N)
[[persistent-segtreePersistent Segtree]]O(N log N)O(log N)
Wavelet TreeO(N logΣ)

핵심 아이디어

각 레벨에서 값 범위를 반으로 나누어 왼쪽 반 (0) / 오른쪽 반 (1) 인지 비트 시퀀스를 만듭니다. 재귀적으로 각 반에 대해 반복.

input: [3 1 4 1 5 9 2 6]  (값 범위 [1..9])

level 0 (mid=5): [0 0 0 0 0 1 0 1]   0→[3 1 4 1 5 2], 1→[9 6]
level 1 왼쪽 (mid=3): [0 0 1 0 1 0]   0→[1 1 2], 1→[3 4 5]
level 1 오른쪽 (mid=7): [1 0]         0→[6], 1→[9]
...

각 노드에 prefix sum 배열 (비트 0 인 원소의 누적 수) 을 저장하면, 쿼리 시 O(1) 에 왼쪽/오른쪽 자식 범위를 계산 가능.

시각화

입력 [3, 1, 4, 1, 5, 9, 2, 6] (값 범위 1..9, 좌표 압축 후 1..8) 에서 Wavelet Tree 구성:

flowchart TD
    A["루트 [1..8]: 3 1 4 1 5 9 2 6\nbits: 0 0 0 0 0 1 0 1"]
    B["왼쪽 [1..4]: 3 1 4 1 5 2\nbits: 0 0 1 0 1 0"]
    C["오른쪽 [5..8]: 9 6\nbits: 1 0"]
    D["[1..2]: 1 1 2"]
    E["[3..4]: 3 4 5"]
    F["[5..6]: 6"]
    G["[7..8]: 9"]
    A --> B
    A --> C
    B --> D
    B --> E
    C --> F
    C --> G

구현 구조

노드 구조

각 Wavelet Tree 노드는 구간 [lo, hi] 에 해당하며:

  • 원소 배열의 순서 (배열 전체를 레벨별로 재배열)
  • cnt[i] = 처음 i개 원소 중 왼쪽 자식 (값 <= mid) 으로 간 원소 수 (prefix sum)

kth 쿼리 알고리즘

kth(node, l, r, k): 현재 노드 범위 [l, r] 에서 k번째 작은 원소

kth(lo, hi, l, r, k):
    if lo == hi: return lo
    mid = (lo + hi) / 2
    // 범위 [l, r] 에서 왼쪽 자식으로 간 원소 수
    left_count = cnt[r+1] - cnt[l]
    if k <= left_count:
        // 왼쪽 자식에서 k번째
        new_l = cnt[l]
        new_r = cnt[r+1] - 1
        return kth(lo, mid, new_l, new_r, k)
    else:
        // 오른쪽 자식에서 (k - left_count)번째
        new_l = (l - cnt[l])                   // 오른쪽 자식 내 인덱스
        new_r = (r + 1 - cnt[r+1]) - 1
        return kth(mid+1, hi, new_l, new_r, k - left_count)

rank 쿼리 알고리즘

rank(l, r, v): A[l..r] 에서 v 보다 작은 원소 수

rank(lo, hi, l, r, v):
    if hi < v: return r - l + 1   // 범위 전체가 v 미만
    if lo >= v: return 0           // 범위 전체가 v 이상
    mid = (lo + hi) / 2
    left_l = cnt[l]
    left_r = cnt[r+1] - 1
    right_l = l - cnt[l]
    right_r = r - cnt[r+1] + 1 - 1
    return rank(lo, mid, left_l, left_r, v) + rank(mid+1, hi, right_l, right_r, v)

구현

// Wavelet Tree: kth smallest in range [l, r]
#include <bits/stdc++.h>
using namespace std;

struct WaveletTree {
  int lo, hi;  // 값 범위 [lo, hi]
  WaveletTree *left = nullptr, *right = nullptr;
  vector<int> cnt;  // cnt[i] = arr[0..i-1] 중 왼쪽(<=mid)으로 간 수

  // arr [from, to) 를 빌드. 좌표 압축된 값 1-indexed 가정.
  void build(int* from, int* to, int lo, int hi) {
      this->lo = lo; this->hi = hi;
      if (lo == hi) return;
      int mid = (lo + hi) / 2;
      // cnt[i] = 처음 i개 중 <= mid 인 수
      cnt.reserve(to - from + 1);
      cnt.push_back(0);
      for (auto it = from; it != to; it++)
          cnt.push_back(cnt.back() + (*it <= mid ? 1 : 0));
      // stable partition: <= mid 먼저, > mid 나중
      auto pivot = stable_partition(from, to, [mid](int x){ return x <= mid; });
      left = new WaveletTree();
      left->build(from, pivot, lo, mid);
      right = new WaveletTree();
      right->build(pivot, to, mid + 1, hi);
  }

  // a[l..r] (0-indexed) 에서 k번째 작은 원소 (1-indexed k)
  int kth(int l, int r, int k) {
      if (lo == hi) return lo;
      int lb = cnt[l];           // [0..l-1] 중 왼쪽으로 간 수
      int rb = cnt[r + 1];       // [0..r] 중 왼쪽으로 간 수
      int left_cnt = rb - lb;    // [l..r] 중 왼쪽으로 간 수
      if (k <= left_cnt)
          return left->kth(lb, rb - 1, k);
      else
          return right->kth(l - lb, r - rb, k - left_cnt);
  }

  // a[l..r] 에서 v보다 작은 원소 수 (rank)
  int rank_lt(int l, int r, int v) {
      if (l > r) return 0;
      if (lo == hi) return (lo < v) ? (r - l + 1) : 0;
      if (hi < v) return r - l + 1;
      if (lo >= v) return 0;
      int mid = (lo + hi) / 2;
      int lb = cnt[l], rb = cnt[r + 1];
      return left->rank_lt(lb, rb - 1, v)
           + right->rank_lt(l - lb, r - rb, v);
  }
};

int main() {
  ios::sync_with_stdio(0); cin.tie(0);
  int n, q; cin >> n >> q;
  vector<int> a(n);
  for (auto& v : a) cin >> v;

  // 좌표 압축
  vector<int> sorted_a = a;
  sort(sorted_a.begin(), sorted_a.end());
  sorted_a.erase(unique(sorted_a.begin(), sorted_a.end()), sorted_a.end());
  int sigma = sorted_a.size();
  for (auto& v : a)
      v = lower_bound(sorted_a.begin(), sorted_a.end(), v) - sorted_a.begin() + 1;

  WaveletTree* wt = new WaveletTree();
  vector<int> tmp = a;
  wt->build(tmp.data(), tmp.data() + n, 1, sigma);

  while (q--) {
      int l, r, k; cin >> l >> r >> k; l--; r--;
      int compressed = wt->kth(l, r, k);
      cout << sorted_a[compressed - 1] << "\n";
  }
}
stdin
5 3
3 1 4 1 5
1 3 2
2 4 1
1 5 3
결과
3
1
3

복잡도

항목
전처리O(N log
공간O(N log
kth 쿼리O(log
rank 쿼리O(log
count in range 쿼리O(log

좌표 압축 후 |Σ| = N 으로 줄일 수 있으므로, 실제로 O(N log N) / O(log N).

지원 쿼리 목록

kth smallest in range

kth(l, r, k): A[l..r] 에서 k 번째로 작은 원소. O(log |Σ|).

count in range

count(l, r, lo, hi): A[l..r] 에서 값 [lo, hi] 인 원소 수.

count(l, r, lo, hi) = rank(l, r, hi+1) - rank(l, r, lo)

rank (이전 값 개수)

rank(l, r, v): A[l..r] 에서 v 보다 작은 원소 수. O(log |Σ|).

K번째 고유값

범위 [l, r] 에서 k 번째로 작은 고유값: kth 를 변형하여 처리.

응용

K-th Smallest in Rectangle

2D 배열에서 특정 사각형 범위의 K번째 작은 원소. Wavelet Tree 를 2D 로 확장하거나, 오프라인으로 처리.

문자열 위 처리

문자열의 접미사 배열(Suffix Array) 위에 Wavelet Tree 를 얹으면 LCP 관련 쿼리를 효율적으로 처리합니다. Suffix Array 참조.

Persistent Segtree 대체

Persistent Segment Tree 와 동일한 쿼리 셋을 지원하지만, 포인터 없이 배열 기반으로 구현 가능해 캐시 친화적입니다.

함정

WARNING

Wavelet Tree 는 정적 자료구조 입니다. 원소 추가/삭제가 있으면 재빌드 필요. 동적 갱신이 필요하면 Balanced BST 기반 Order Statistics Tree 를 고려하세요.

1. 좌표 압축 필수

값 범위가 크면 (최대 10^9) 좌표 압축 없이는 메모리 초과. 압축 후 |Σ| = N 으로 만드세요.

2. stable_partition 필수

빌드 시 원소 순서를 보존해야 합니다. 단순 partition 은 순서를 망가뜨립니다.

3. 인덱스 범위 오류

cnt 배열은 크기 N+1. cnt[r+1] 접근 시 경계 초과 주의.

4. 재귀 깊이

Python 에서 |Σ| = N = 10^5 이면 깊이 약 17. sys.setrecursionlimit(10**6) 필수.

BOJ 연습 문제

번호제목링크
BOJ 7469K번째 수BOJ
BOJ 13537수열과 쿼리 1BOJ

참고

이 글의 용어 (5개)
머지 소트 트리 (Merge Sort Tree)algorithm
정의 머지 소트 트리 (Merge Sort Tree) 는 세그먼트 트리의 각 노드에 해당 구간의 정렬된 부분 배열을 저장하는 자료구조. 구간 내에서 k 이하 원소의 개수, k번째…
Order Statistics Tree (OST): rank/select 지원 BSTalgorithm
정의 Order Statistics Tree (OST) 는 각 노드에 서브트리 크기 (size) 를 추가로 저장한 균형 BST. 두 가지 새 연산을 O(log N) 에 지원한다.…
Persistent Segment Tree (영속 세그): 시점별 스냅샷algorithm
정의 Persistent Segment Tree 는 매 갱신마다 이전 버전을 그대로 유지하면서 새 버전을 만드는 세그먼트 트리입니다. 각 버전은 루트 포인터 로 식별됩니다. 핵심…
Suffix Array (접미사 배열)algorithm
정의 Suffix Array (SA, 접미사 배열) 는 문자열 s 의 모든 접미사를 사전순 정렬한 인덱스 배열. SA[i] = 시작 위치 (0-indexed). 보통 LCP (L…
Top K Selection: 상위 K개 원소algorithm
정의 배열 또는 스트림에서 상위 K 개 원소를 뽑는 문제. 혹은 K 번째로 큰/작은 원소 하나만 찾는 선택(selection) 문제. - 오프라인: 배열 전체가 주어짐 → Qui…

💬 댓글

사이트 검색 / 명령어

검색

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