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

카테시안 트리 (Cartesian Tree)

· 수정 · 📖 약 4분 · 1,238자/단어 #algorithm #data-structure #cartesian-tree #tree #rmq
cartesian tree, 카테시안 트리, Treap (배열)

정의

카테시안 트리 (Cartesian Tree) 는 배열 a[0..N-1] 로부터 만드는 이진 트리로, 다음 두 성질을 동시에 만족:

  1. In-order 순회 = 원래 배열 순서 (BST의 key 순서)
  2. 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";
  }
}
stdin
5 3
3 1 4 2 5
1 3
2 5
1 5
결과
1
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

참고

이 글의 용어 (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 등으로 자연스럽게 일반화됩니다. 방법 비…

💬 댓글

사이트 검색 / 명령어

검색

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