Implicit Treap: 배열 인덱스 기반 Treap
정의
Implicit Treap 은 Treap 에서 키 대신 서브트리 크기 (위치) 로 인덱싱하는 변형입니다. 배열의 삽입/삭제/이동/구간 반전 등을 O(log N) 에 지원합니다.
각 노드에 명시적 키를 저장하지 않고, 서브트리 크기 (sz) 로부터 암묵적으로 인덱스를 계산합니다. 현재 노드의 0-indexed 인덱스 = 왼쪽 서브트리 크기.
문제 상황과 동기
일반 배열은 임의 위치 접근이 O(1)이지만 삽입/삭제가 O(N)입니다. 연결 리스트는 삽입/삭제가 O(1)이지만 임의 접근이 O(N)입니다. Implicit Treap 은 두 연산을 모두 O(log N) 으로 지원합니다.
| 연산 | 일반 배열 | 연결 리스트 | Implicit Treap |
|---|---|---|---|
| 임의 위치 접근 | O(1) | O(N) | O(log N) |
| 임의 위치 삽입/삭제 | O(N) | O(N)* | O(log N) |
| 구간 반전 | O(N) | O(N) | O(log N) |
| 구간 합/최소/최대 | O(N) | O(N) | O(log N) |
*탐색 O(N) + 삽입 O(1). 위치를 알더라도 단순 배열보다 상수가 크다.
대표 응용: 임의 위치 삽입/삭제, 구간 반전, 구간 이동, Rope (문자열 편집기).
시각화
배열 [A, B, C, D, E] 의 Implicit Treap 표현 (pri = 랜덤 우선순위, sz = 서브트리 크기):
flowchart TD
C["val=C, pri=91, sz=5"]
A["val=A, pri=75, sz=2"]
E["val=E, pri=63, sz=2"]
B["val=B, pri=48, sz=1"]
D["val=D, pri=55, sz=1"]
C --> A
C --> E
A --> nil1["nil"]
A --> B
E --> D
E --> nil2["nil"]
- 중위 순회 (inorder) 하면
A B C D E순서 유지 - pri 기준 최대 힙: 부모 pri > 자식 pri
- sz = 해당 노드를 포함한 서브트리 총 노드 수
insert(root, 2, X) 수행 과정 (0-indexed pos=2 에 X 삽입):
flowchart TD
T0["T = [A, B, C, D, E]"]
T0 -->|"split(T, 2)"| S["L=[A,B] + R=[C,D,E]"]
S -->|"merge(L, new X)"| M["[A, B, X]"]
M -->|"merge with R"| Result["[A, B, X, C, D, E]"]
핵심 아이디어
암묵적 인덱스 (Implicit Key)
일반 BST 는 키로 순서를 결정합니다. Implicit Treap 에서는 키를 저장하지 않고, 서브트리 크기 로 순서를 결정합니다.
// 현재 노드의 0-indexed 위치
index(node) = sz(node.left)
루트에서 왼쪽으로 갈 때마다 인덱스가 줄고, 오른쪽으로 갈 때마다 늘어납니다. 이 성질로 임의 위치를 O(log N) 에 찾을 수 있습니다.
split(t, k)
트리 t 를 앞 k 개 와 나머지 로 분리합니다. O(log N).
split(t, k): // t 앞 k 개 노드를 L로, 나머지를 R로 분리
if t == nil: return (nil, nil)
push(t) // lazy propagation flush
lsz = sz(t.left)
if lsz >= k:
(L, R) = split(t.left, k)
t.left = R; upd(t)
return (L, t)
else:
(L, R) = split(t.right, k - lsz - 1)
t.right = L; upd(t)
return (t, R)
merge(a, b)
두 트리 a, b 를 이어붙입니다 (a 의 모든 원소가 b 보다 앞에 위치). O(log N).
merge(a, b):
if a == nil: return b
if b == nil: return a
push(a); push(b)
if a.pri > b.pri: // a 가 힙 상위 -> a 가 루트
a.right = merge(a.right, b); upd(a); return a
else:
b.left = merge(a, b.left); upd(b); return b
Lazy Propagation: 구간 반전
구간 반전 시 서브트리의 left/right 를 swap 합니다. lazy flag (rev) 로 실제 swap 을 노드 방문 시점까지 미룹니다.
push(t):
if t.rev:
swap(t.left, t.right)
if t.left: t.left.rev ^= 1
if t.right: t.right.rev ^= 1
t.rev = false
// split/merge 에서 노드 방문 전에 반드시 push 호출
삽입 / 삭제
insert(t, pos, val): // 0-indexed pos 위치에 val 삽입
(L, R) = split(t, pos)
return merge(merge(L, new Node(val)), R)
erase(t, pos): // 0-indexed pos 위치 삭제
(L, MR) = split(t, pos)
(M, R) = split(MR, 1)
return merge(L, R) // M 은 삭제
알고리즘
구간 뒤집기 (Range Reversal)
reverse(t, l, r): // [l, r] 구간 반전, 0-indexed
(L, MR) = split(t, l)
(M, R) = split(MR, r - l + 1)
M.rev ^= 1 // lazy flag 설정
return merge(merge(L, M), R)
구간 이동 (Range Move)
move(t, l, r, dest): // [l, r] 을 위치 dest 앞으로 이동
(L, MR) = split(t, l)
(M, R) = split(MR, r - l + 1)
t2 = merge(L, R) // M 제거
(LL, RR) = split(t2, dest) // dest 위치에 M 삽입
return merge(merge(LL, M), RR)
구현
// Implicit Treap: 삽입(I pos val), 삭제(D pos), 구간 반전(R l r)
#include <bits/stdc++.h>
using namespace std;
mt19937 rng(42);
struct Node {
int val, pri, sz;
bool rev;
Node *l, *r;
Node(int v) : val(v), pri(rng()), sz(1), rev(false), l(nullptr), r(nullptr) {}
};
int sz(Node* t) { return t ? t->sz : 0; }
void upd(Node* t) {
if (t) t->sz = 1 + sz(t->l) + sz(t->r);
}
void push(Node* t) {
if (!t || !t->rev) return;
swap(t->l, t->r);
if (t->l) t->l->rev ^= 1;
if (t->r) t->r->rev ^= 1;
t->rev = false;
}
pair<Node*, Node*> split(Node* t, int k) {
if (!t) return {nullptr, nullptr};
push(t);
int lsz = sz(t->l);
if (lsz >= k) {
auto [L, R] = split(t->l, k);
t->l = R; upd(t);
return {L, t};
} else {
auto [L, R] = split(t->r, k - lsz - 1);
t->r = L; upd(t);
return {t, R};
}
}
Node* merge(Node* l, Node* r) {
if (!l) return r;
if (!r) return l;
push(l); push(r);
if (l->pri > r->pri) {
l->r = merge(l->r, r); upd(l); return l;
} else {
r->l = merge(l, r->l); upd(r); return r;
}
}
Node* ins(Node* t, int pos, int v) {
auto [L, R] = split(t, pos);
return merge(merge(L, new Node(v)), R);
}
Node* era(Node* t, int pos) {
auto [L, MR] = split(t, pos);
auto [M, R] = split(MR, 1);
delete M;
return merge(L, R);
}
Node* rev(Node* t, int l, int r) {
auto [L, MR] = split(t, l);
auto [M, R] = split(MR, r - l + 1);
if (M) M->rev ^= 1;
return merge(merge(L, M), R);
}
void print(Node* t) {
if (!t) return;
push(t);
print(t->l);
cout << t->val << " ";
print(t->r);
}
int main() {
ios::sync_with_stdio(0); cin.tie(0);
int n; cin >> n;
Node* root = nullptr;
for (int i = 0; i < n; i++) {
int v; cin >> v;
root = ins(root, i, v);
}
int q; cin >> q;
while (q--) {
char op; cin >> op;
if (op == 'I') {
int pos, v; cin >> pos >> v;
root = ins(root, pos, v);
} else if (op == 'D') {
int pos; cin >> pos;
root = era(root, pos);
} else {
int l, r; cin >> l >> r;
root = rev(root, l, r);
}
}
print(root); cout << "\n";
}5
1 2 3 4 5
3
R 1 3
I 2 9
D 41 4 9 3 5복잡도
| 항목 | 값 |
|---|---|
| split | O(log N) 기대 |
| merge | O(log N) 기대 |
| insert / erase | O(log N) 기대 |
| range reverse | O(log N) 기대 |
| 공간 | O(N) |
Treap 의 기대 높이 = O(log N). 랜덤 우선순위로 편향 트리 확률이 극히 낮습니다.
변형 / 활용
| 응용 | 설명 |
|---|---|
| Rope | 문자열 편집기. 임의 위치 삽입/삭제/분할/합병. |
| 구간 lazy 연산 | 구간 덧셈, 구간 최솟값 등 [[lazyprop |
| 순서 통계 | k 번째 원소 조회, 원소의 순위 조회. |
| 구간 이동 | 배열 구간을 다른 위치로 이동. split 3번 + merge 2번. |
| Persistent | 버전 관리. 노드 복사로 과거 상태 유지. |
함정
WARNING
Implicit Treap 구현 시 자주 발생하는 실수들.
1. push 누락
split/merge 에서 노드 방문 전에 push 를 호출하지 않으면 lazy flag 가 잘못 전파됩니다. 구간 반전 결과가 틀립니다.
2. upd 누락
split/merge 에서 자식 포인터를 변경한 뒤 upd 를 호출하지 않으면 sz 가 틀립니다. 이후 모든 위치 계산이 틀립니다.
3. 0-indexed vs 1-indexed 혼용
split(t, k) 는 앞 k 개를 분리합니다. 0-indexed pos 에 삽입하려면 split(t, pos) 입니다. 1-indexed 로 혼용하면 off-by-one 버그가 납니다.
4. 재귀 깊이 (Python)
Python 기본 재귀 한도 1000. N=10^5 이면 sys.setrecursionlimit(300000) 필요. 또는 반복 구현 사용.
5. 메모리 누수 (C++)
erase 에서 삭제된 노드를 delete 하지 않으면 메모리 누수. 또는 메모리 풀 (배열 기반) 사용.
BOJ 연습 문제
| 번호 | 제목 | 설명 |
|---|---|---|
| BOJ 13159 | 배열 | 삽입/삭제/반전 지원 배열 |
| BOJ 1655 | 가운데를 말해요 | 중앙값 유지 (treap 응용) |
| BOJ 7469 | K번째 수 | 구간 k번째 수 (merge sort tree 또는 treap) |
관련 위키
이 글의 용어 (6개)
- 레이지 프로파게이션 (Lazy Propagation)algorithm
- 정의 레이지 프로파게이션 (Lazy Propagation) 은 세그먼트 트리의 확장으로, 구간 갱신 (range update) 와 구간 쿼리 (range query) 를 모두 O…
- 로프 (Rope)algorithm
- 정의 로프 (Rope) 는 문자열을 이진 트리로 표현해 split (분할), concat (연결) 을 O(log N) 에 처리하는 자료구조. 각 리프는 짧은 문자열 조각, 내부 …
- 세그먼트 트리 (Segment Tree)algorithm
- 정의 세그먼트 트리 (Segment Tree) 는 배열의 구간 쿼리 (range query) 와 점 갱신 (point update) 를 모두 O(log N) 에 처리하는 이진 트…
- 연결 리스트 (Linked List)algorithm
- 정의 연결 리스트 (Linked List) 는 노드 + 포인터로 구성된 선형 자료구조. 각 노드는 데이터 + 다음 노드 포인터. 종류: - Singly Linked List: 노…
- BBST (Splay Tree, Treap)algorithm
- 정의 BBST (Balanced Binary Search Tree) 는 균형이 amortized / expected 로 보장되는 이진 탐색 트리. PS 에서는 split / me…
- Binary Search Tree (BST): 정렬된 이진 트리algorithm
- 정의 Binary Search Tree (BST) 는 각 노드가 다음 BST 불변식을 만족하는 이진 트리: - 왼쪽 서브트리의 모든 키 < 노드 키 - 오른쪽 서브트리의 모든 키…
💬 댓글