가장자리 트리(지속 가능한 세그먼트 트리) 기초 설명

개요

가장자리 트리는 지속 가능한 세그먼트 트리로, 하지타 님이 개발한 데이터 구조이다. 함수형 세그먼트 트리라고도 불린다. 이 구조는 배열의 역사적 상태를 효율적으로 저장하고 조회할 수 있게 해준다.

핵심 아이디어

모든 버전을 복제하는 방식은 메모리 낭비가 심하다. 하지만 각 업데이트에서 영향을 받는 노드는 루트까지의 경로에 국한된다. 따라서 기존 트리와 공유되는 부분은 그대로 유지하고, 변경된 경로만 새롭게 연결하여 저장하면 된다. 이로 인해 공간 복잡도는 O(n log n) 수준으로 유지된다.

예제 1: 순서 기반 중위값 조회 (P3834)

배열의 특정 구간에서 k번째로 작은 값을 찾는 문제. 가중치 기반의 지속 가능한 세그먼트 트리를 사용한다.


#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;
const int MAXT = 32 * MAXN;

int n, m, arr[MAXN], roots[MAXN];
struct TreeNode {
    int left, right, count;
} tree[MAXT];
int node_cnt = 0;

int insert(int root_id, int l, int r, int pos, int delta) {
    int curr = ++node_cnt;
    tree[curr] = tree[root_id]; // 기존 노드 복사

    if (l == r) {
        tree[curr].count += delta;
        return curr;
    }

    int mid = (l + r) / 2;
    if (pos <= mid) {
        tree[curr].left = insert(tree[root_id].left, l, mid, pos, delta);
    } else {
        tree[curr].right = insert(tree[root_id].right, mid + 1, r, pos, delta);
    }
    tree[curr].count = tree[tree[curr].left].count + tree[tree[curr].right].count;
    return curr;
}

int query(int left_root, int right_root, int l, int r, int k) {
    if (l == r) return l;
    int mid = (l + r) / 2;
    int left_count = tree[tree[left_root].left].count - tree[tree[right_root].left].count;

    if (k <= left_count) {
        return query(tree[left_root].left, tree[right_root].left, l, mid, k);
    } else {
        return query(tree[left_root].right, tree[right_root].right, mid + 1, r, k - left_count);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    vector<int> values;
    for (int i = 1; i <= n; ++i) {
        cin >> arr[i];
        values.push_back(arr[i]);
    }

    sort(values.begin(), values.end());
    values.erase(unique(values.begin(), values.end()), values.end());

    // 좌표 압축
    for (int i = 1; i <= n; ++i) {
        arr[i] = lower_bound(values.begin(), values.end(), arr[i]) - values.begin() + 1;
    }

    // 초기 루트 생성
    roots[0] = 0;
    for (int i = 1; i <= n; ++i) {
        roots[i] = insert(roots[i-1], 1, values.size(), arr[i], 1);
    }

    while (m--) {
        int l, r, k;
        cin >> l >> r >> k;
        int result_idx = query(roots[r], roots[l-1], 1, values.size(), k);
        cout << values[result_idx - 1] << '\n';
    }

    return 0;
}

예제 2: 지속 가능한 배열 (P3919)

배열의 특정 위치에 값을 설정하거나, 과거 버전의 값을 조회하는 문제. 각 버전이 독립적인 트리로 표현된다.


#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1000005;
const int MAXT = 32 * MAXN;

int n, m, base_arr[MAXN], roots[MAXN];
struct Node {
    int left, right, value;
} nodes[MAXT];
int node_idx = 0;

int build_tree(int l, int r) {
    int curr = ++node_idx;
    if (l == r) {
        nodes[curr].value = base_arr[l];
        return curr;
    }
    int mid = (l + r) / 2;
    nodes[curr].left = build_tree(l, mid);
    nodes[curr].right = build_tree(mid + 1, r);
    return curr;
}

int update(int old_root, int l, int r, int pos, int new_val) {
    int curr = ++node_idx;
    nodes[curr] = nodes[old_root];

    if (l == r) {
        nodes[curr].value = new_val;
        return curr;
    }

    int mid = (l + r) / 2;
    if (pos <= mid) {
        nodes[curr].left = update(nodes[old_root].left, l, mid, pos, new_val);
    } else {
        nodes[curr].right = update(nodes[old_root].right, mid + 1, r, pos, new_val);
    }
    return curr;
}

int get_value(int root, int l, int r, int pos) {
    if (l == r) return nodes[root].value;
    int mid = (l + r) / 2;
    if (pos <= mid) {
        return get_value(nodes[root].left, l, mid, pos);
    } else {
        return get_value(nodes[root].right, mid + 1, r, pos);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {
        cin >> base_arr[i];
    }

    roots[0] = build_tree(1, n);
    for (int i = 1; i <= m; ++i) {
        int ver, op, idx;
        cin >> ver >> op >> idx;
        if (op == 1) {
            int val;
            cin >> val;
            roots[i] = update(roots[ver], 1, n, idx, val);
        } else {
            cout << get_value(roots[ver], 1, n, idx) << '\n';
            roots[i] = roots[ver];
        }
    }

    return 0;
}

세그먼트 트리 상의 이분 탐색 기법

여러 버전의 트리에서 특정 조건을 만족하는 값의 위치를 찾는 데 유용하다.


int find_first(int a, int b, int c, int d, int l, int r, int low, int high) {
    if ((tree[a].count ^ tree[b].count ^ tree[c].count ^ tree[d].count) == 0)
        return -1;

    if (l == r) return l;

    int mid = (l + r) / 2;
    if (low <= mid) {
        int res = find_first(tree[a].left, tree[b].left, tree[c].left, tree[d].left, l, mid, low, high);
        if (res != -1) return res;
    }
    if (high > mid) {
        int res = find_first(tree[a].right, tree[b].right, tree[c].right, tree[d].right, mid + 1, r, low, high);
        if (res != -1) return res;
    }
    return -1;
}

태그: 가장자리 트리 지속 가능한 세그먼트 트리 좌표 압축 이분 탐색 함수형 자료구조

9월 9일 16:17에 게시됨