kz-maxx

Декартово дерево

Treap на массивах: split через pair<int, int>, merge, insert, erase, find, kth

Объяснение

Декартово дерево, или Treap, — это структура данных, которая соединяет две идеи:

  • Binary Search Tree — по значению key
  • Heap — по случайному приоритету prior
По key: слева лежат меньшие значения, справа — большие. По prior: у родителя приоритет больше, чем у детей.
key: left < root < right
prior: root.prior > child.prior

Благодаря случайным приоритетам дерево обычно получается сбалансированным. Поэтому основные операции работают за O(log n) в среднем.

Что храним в массиве?

struct Node {
    int key, prior;
    int l, r;
    int cnt;
} tr[N];

int root;
  • key — значение вершины
  • prior — случайный приоритет
  • l — индекс левого сына
  • r — индекс правого сына
  • cnt — размер поддерева
  • root — корень всего дерева
Индекс 0 означает пустую вершину. Это как nullptr, только для массива.

Главные операции

split

split(v, x) делит дерево v на две части:

  • первая часть — все значения <= x
  • вторая часть — все значения > x
split(root, 10) → {дерево <= 10, дерево > 10}

merge

merge(a, b) соединяет два дерева в одно.

Важно: все значения в a должны быть меньше всех значений в b.
merge(a, b) → одно дерево

Пример задачи для реализации

Нужно хранить множество чисел и обрабатывать команды.

Команда Что делает
insert x Добавить число x
erase x Удалить число x
find x Проверить, есть ли x
kth k Найти k-й элемент по возрастанию

Input

10
insert 10
insert 5
insert 20
find 10
find 7
kth 2
erase 10
find 10
kth 2
find 20

Output

YES
NO
10
NO
20
YES

Полная реализация

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

const int N = 100010;

struct Node {
    int key, prior;
    int l, r;
    int cnt;
} tr[N];

int root;
int idx;

int sizeOf(int v) {
    if (v == 0) {
        return 0;
    }

    return tr[v].cnt;
}

void pull(int v) {
    if (v == 0) {
        return;
    }

    tr[v].cnt = 1 + sizeOf(tr[v].l) + sizeOf(tr[v].r);
}

int newNode(int x) {
    idx++;

    tr[idx].key = x;
    tr[idx].prior = rand();
    tr[idx].l = 0;
    tr[idx].r = 0;
    tr[idx].cnt = 1;

    return idx;
}

pair<int, int> split(int v, int x) {
    if (v == 0) {
        return {0, 0};
    }

    if (tr[v].key <= x) {
        pair<int, int> p = split(tr[v].r, x);

        tr[v].r = p.first;
        pull(v);

        return {v, p.second};
    } else {
        pair<int, int> p = split(tr[v].l, x);

        tr[v].l = p.second;
        pull(v);

        return {p.first, v};
    }
}

int merge(int a, int b) {
    if (a == 0) {
        return b;
    }

    if (b == 0) {
        return a;
    }

    if (tr[a].prior > tr[b].prior) {
        tr[a].r = merge(tr[a].r, b);
        pull(a);

        return a;
    } else {
        tr[b].l = merge(a, tr[b].l);
        pull(b);

        return b;
    }
}

bool findKey(int v, int x) {
    if (v == 0) {
        return false;
    }

    if (tr[v].key == x) {
        return true;
    }

    if (x < tr[v].key) {
        return findKey(tr[v].l, x);
    } else {
        return findKey(tr[v].r, x);
    }
}

void insertKey(int x) {
    if (findKey(root, x)) {
        return;
    }

    pair<int, int> p = split(root, x);

    int leftTree = p.first;
    int rightTree = p.second;
    int node = newNode(x);

    root = merge(merge(leftTree, node), rightTree);
}

void eraseKey(int &v, int x) {
    if (v == 0) {
        return;
    }

    if (tr[v].key == x) {
        v = merge(tr[v].l, tr[v].r);
        return;
    }

    if (x < tr[v].key) {
        eraseKey(tr[v].l, x);
    } else {
        eraseKey(tr[v].r, x);
    }

    pull(v);
}

int kth(int v, int k) {
    int leftSize = sizeOf(tr[v].l);

    if (k == leftSize + 1) {
        return tr[v].key;
    }

    if (k <= leftSize) {
        return kth(tr[v].l, k);
    }

    return kth(tr[v].r, k - leftSize - 1);
}

void printTree(int v) {
    if (v == 0) {
        return;
    }

    printTree(tr[v].l);
    cout << tr[v].key << ' ';
    printTree(tr[v].r);
}

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

    srand(time(0));

    int q;
    cin >> q;

    while (q--) {
        string op;
        int x;

        cin >> op >> x;

        if (op == "insert") {
            insertKey(x);
        } else if (op == "erase") {
            eraseKey(root, x);
        } else if (op == "find") {
            if (findKey(root, x)) {
                cout << "YES\n";
            } else {
                cout << "NO\n";
            }
        } else if (op == "kth") {
            if (x >= 1 && x <= sizeOf(root)) {
                cout << kth(root, x) << '\n';
            }
        }
    }

    return 0;
}

Объяснение функций

1. sizeOf

int sizeOf(int v) {
    if (v == 0) {
        return 0;
    }

    return tr[v].cnt;
}

Функция возвращает размер поддерева. Если вершина пустая, размер равен 0.

sizeOf(0) = 0
sizeOf(v) = tr[v].cnt

2. pull

void pull(int v) {
    tr[v].cnt = 1 + sizeOf(tr[v].l) + sizeOf(tr[v].r);
}

Функция пересчитывает размер поддерева вершины v.

если слева 2 вершины, справа 3 вершины
cnt = 1 + 2 + 3 = 6

3. newNode

int newNode(int x)

Создает новую вершину в массиве tr. Возвращает индекс этой вершины.

newNode(10)
key = 10
l = 0, r = 0
cnt = 1

4. split

pair<int, int> split(int v, int x)

Делит дерево на две части:

  • первая часть содержит значения <= x
  • вторая часть содержит значения > x
дерево: 3 5 8 10 15
split(root, 8)
leftTree: 3 5 8
rightTree: 10 15

Если текущий key <= x, значит эта вершина должна попасть в левую часть. Тогда мы делим её правого сына.

if (tr[v].key <= x) {
    pair<int, int> p = split(tr[v].r, x);

    tr[v].r = p.first;
    pull(v);

    return {v, p.second};
}

Если текущий key > x, значит эта вершина должна попасть в правую часть. Тогда мы делим её левого сына.

else {
    pair<int, int> p = split(tr[v].l, x);

    tr[v].l = p.second;
    pull(v);

    return {p.first, v};
}

5. merge

int merge(int a, int b)

Соединяет два дерева. Все значения в a должны быть меньше всех значений в b.

a: 1 3 5
b: 8 10 12
merge(a, b): 1 3 5 8 10 12

Корнем становится дерево с большим prior.

if (tr[a].prior > tr[b].prior) {
    tr[a].r = merge(tr[a].r, b);
    pull(a);

    return a;
}

Если у b приоритет больше, то b становится корнем.

else {
    tr[b].l = merge(a, tr[b].l);
    pull(b);

    return b;
}

6. findKey

bool findKey(int v, int x)

Ищет число x как в обычном бинарном дереве поиска.

если x меньше key → идем влево
если x больше key → идем вправо
если x == key → нашли

7. insertKey

void insertKey(int x)

Добавляет число x в дерево.

было: 3 5 10
insert 7
split(root, 7): 3 5 и 10
потом: merge(3 5, 7, 10)
стало: 3 5 7 10
pair<int, int> p = split(root, x);

int leftTree = p.first;
int rightTree = p.second;
int node = newNode(x);

root = merge(merge(leftTree, node), rightTree);

8. eraseKey

void eraseKey(int &v, int x)

Удаляет число x из дерева. Когда нашли нужную вершину, заменяем её на merge левого и правого сына.

было: left ← x → right
стало: merge(left, right)
if (tr[v].key == x) {
    v = merge(tr[v].l, tr[v].r);
    return;
}

9. kth

int kth(int v, int k)

Находит k-й элемент по возрастанию. Для этого используется размер левого поддерева.

дерево: 3 5 7 10 20
kth(1) = 3
kth(3) = 7
kth(5) = 20

Если слева стоит leftSize элементов:

  • если k == leftSize + 1, ответ — текущая вершина
  • если k <= leftSize, идем влево
  • иначе идем вправо и уменьшаем k

Асимптотика

Операция Среднее время
insert O(log n)
erase O(log n)
find O(log n)
kth O(log n)
split / merge O(log n)