카테시안 트리 (Cartesian Tree)
정의
카테시안 트리 (Cartesian Tree) 는 배열 a[0..N-1] 로부터 만드는 이진 트리로, 다음 두 성질을 동시에 만족:
- In-order 순회 = 원래 배열 순서 (BST의 key 순서)
- Heap 성질: 각 노드의 값이 그 서브트리 내 최소(또는 최대)
결과적으로 루트는 배열의 전역 최솟값, 각 서브트리의 루트는 해당 구간의 최솟값. RMQ (Range Minimum Query) 를 LCA (Lowest Common Ancestor) 로 변환하거나, 구간 DP / 분할 정복의 재귀 구조를 트리로 시각화할 때 사용.
1980년대 Jean Vuillemin이 정의, PS에서는 RMQ-LCA 등가성과 함께 2000년대 중반부터 활용.
문제 상황과 동기
배열 a[]가 주어졌을 때:
- RMQ 쿼리 “구간 [l, r]의 최솟값”을 O(log N)으로 처리하고 싶다.
- naive: Sparse Table O(N log N) 전처리 + O(1) 쿼리 또는 세그트리 O(N) + O(log N).
- Cartesian Tree: O(N) 빌드 + LCA O(log N) 또는 Sparse Table on LCA O(1).
핵심 통찰: 배열을 트리로 변환하면 RMQ(l, r) = a[LCA(l, r)]. LCA는 이미 O(log N) 알고리즘이 많으므로 재사용 가능.
추가 응용: 히스토그램에서 최대 직사각형 (분할 정복 구조), 괄호 시퀀스 매칭, 배열의 재귀적 분할 정복 시각화.
시각화
핵심 아이디어
invariant: 루트 = 구간 최솟값, 왼쪽 서브트리 = 루트 왼쪽 원소들, 오른쪽 = 오른쪽 원소들. 재귀 분할.
build(l, r):
if l > r: return null
m = argmin(a[l..r]) # 구간 최솟값 인덱스
node = new Node(a[m])
node.left = build(l, m-1)
node.right = build(m+1, r)
return node
naive O(N^2), 스택 최적화 로 O(N):
stack = []
for i = 0..N-1:
last = null
while stack not empty and a[stack.top] >= a[i]:
last = stack.pop()
if stack not empty:
stack.top.right = node(i)
if last:
node(i).left = last
stack.push(node(i))
root = stack.bottom
RMQ(l, r) = a[LCA(노드_l, 노드_r)]. LCA는 Binary Lifting / Sparse Table 로 O(log N) 또는 O(1).
알고리즘
# O(N) 빌드 (스택)
build_cartesian(a):
stack = []
for i in 0..N-1:
last = null
while stack not empty and a[stack.top.idx] >= a[i]:
last = stack.pop()
node = Node(i, a[i])
if stack:
stack.top.right = node
if last:
node.left = last
stack.push(node)
return stack[0] # 루트
# RMQ via LCA
RMQ(l, r):
return a[LCA(node_l, node_r).idx]
구현
// Cartesian Tree: O(N) 빌드, RMQ via naive LCA O(log N)
#include <bits/stdc++.h>
using namespace std;
struct Node {
int idx, val;
Node *left = nullptr, *right = nullptr;
Node(int i, int v) : idx(i), val(v) {}
};
Node* buildCartesian(const vector<int>& a) {
stack<Node*> st;
for (int i = 0; i < (int)a.size(); i++) {
Node* last = nullptr;
while (!st.empty() && a[st.top()->idx] >= a[i]) {
last = st.top(); st.pop();
}
Node* node = new Node(i, a[i]);
if (!st.empty()) st.top()->right = node;
if (last) node->left = last;
st.push(node);
}
// 루트는 스택 맨 아래
while (st.size() > 1) st.pop();
return st.empty() ? nullptr : st.top();
}
// naive LCA (부모 포인터 없이 재귀)
Node* LCA(Node* root, int l, int r) {
if (!root || root->idx < l || root->idx > r) return nullptr;
if (l <= root->idx && root->idx <= r) return root;
Node* left_lca = LCA(root->left, l, r);
Node* right_lca = LCA(root->right, l, r);
if (left_lca && right_lca) return root;
return left_lca ? left_lca : right_lca;
}
int RMQ(Node* root, int l, int r) {
Node* lca = LCA(root, l, r);
return lca ? lca->val : INT_MAX;
}
int main() {
int n, q; cin >> n >> q;
vector<int> a(n);
for (auto& v : a) cin >> v;
Node* root = buildCartesian(a);
while (q--) {
int l, r; cin >> l >> r;
cout << RMQ(root, l - 1, r - 1) << "\n";
}
}5 3
3 1 4 2 5
1 3
2 5
1 51
1
1복잡도
| 항목 | 값 |
|---|---|
| 빌드 (스택) | O(N) 시간, O(N) 공간 |
| 빌드 (naive 재귀) | O(N^2) 최악 (정렬 배열) |
| RMQ (naive LCA) | O(N) 최악 |
| RMQ (Binary Lifting LCA) | O(log N) |
| RMQ (Sparse Table LCA) | O(1) after O(N log N) 전처리 |
스택 빌드는 각 원소 push/pop 1회씩, 총 O(N). RMQ는 LCA 방법에 따라 달라짐.
변형 / 활용
1. RMQ-LCA 등가
RMQ(l, r) = a[LCA(l, r)]. LCA 문제를 RMQ로 변환 (Euler Tour + Sparse Table) 또는 그 역.
2. 히스토그램 최대 직사각형
분할 정복: 최솟값을 기준으로 좌우 분할. Cartesian Tree로 재귀 구조 명시.
3. Treap (랜덤 우선순위)
배열 대신 삽입 순서 + 랜덤 우선순위로 Cartesian Tree. BST + Heap.
4. 괄호 시퀀스
올바른 괄호는 Cartesian Tree와 bijection. 트리 ↔ 괄호 변환.
함정
1. 중복 최솟값
a[i] = a[j] 인 경우 ”≥” 연산자로 왼쪽 우선 또는 오른쪽 우선 일관성 필요. 구현에서 >= vs > 선택.
2. 1-indexed vs 0-indexed
노드 idx는 0-indexed, 쿼리 입력은 1-indexed. 변환 필수.
3. LCA 최적화 미비
naive LCA O(N)이면 RMQ O(N)이 되어 의미 없음. Binary Lifting / Sparse Table 필수.
4. 스택 빌드 역순 연결
스택 맨 아래가 루트. stack[0] 리턴 전에 스택 정리 필요.
BOJ 연습 문제
| 번호 | 제목 | 정답률 | 링크 |
|---|---|---|---|
| BOJ 1738 | 골목길 | - | kokoa-lab |
| BOJ 6549 | 히스토그램에서 가장 큰 직사각형 | - | kokoa-lab |
| BOJ 1289 | 트리의 가중치 | - | kokoa-lab |
참고
- Range Minimum Query
- Lowest Common Ancestor
- 희소 배열
- 세그먼트 트리
- Treap (Cartesian Tree의 확률적 변형)
이 글의 용어 (5개)
- 세그먼트 트리 (Segment Tree)algorithm
- 정의 세그먼트 트리 (Segment Tree) 는 배열의 구간 쿼리 (range query) 와 점 갱신 (point update) 를 모두 O(log N) 에 처리하는 이진 트…
- 최소 공통 조상 (Lowest Common Ancestor)algorithm
- 정의 최소 공통 조상 (LCA, Lowest Common Ancestor) 는 트리에서 두 노드 u, v 의 공통 조상 중 가장 깊은 (루트에서 가장 먼) 노드. Binary L…
- 희소 배열 (Sparse Table)algorithm
- 정의 희소 배열 (Sparse Table) 은 정적 배열에서 결합 법칙을 만족하는 idempotent 연산 (min, max, gcd, lcm 등) 의 구간 쿼리를 O(1) 시간…
- BBST (Splay Tree, Treap)algorithm
- 정의 BBST (Balanced Binary Search Tree) 는 균형이 amortized / expected 로 보장되는 이진 탐색 트리. PS 에서는 split / me…
- RMQ (Range Minimum Query)algorithm
- 정의 Range Minimum Query 는 배열 A 에 대해 구간 [l, r] 의 최솟값을 답하는 문제입니다. 최댓값, gcd, XOR 등으로 자연스럽게 일반화됩니다. 방법 비…
💬 댓글