Дерево слияний и порядковые статистики
В узле хранится не число, а отсортированный массив: сколько чисел меньше x и какое k-е по величине.
3 мин
Пока в узле лежало одно число, дерево отвечало на вопросы про сумму и максимум. Теперь положим в узел отсортированный массив всех элементов его отрезка.
Строится это снизу вверх слиянием, как в сортировке слиянием, за . Столько же занимает память: каждый элемент лежит на всех уровнях по одному разу.
void build(int v, int tl, int tr) {
if (tl == tr) { tree[v] = {a[tl]}; return; }
int tm = (tl + tr) / 2;
build(2 * v, tl, tm);
build(2 * v + 1, tm + 1, tr);
tree[v].resize(tr - tl + 1);
std::merge(tree[2 * v].begin(), tree[2 * v].end(),
tree[2 * v + 1].begin(), tree[2 * v + 1].end(), tree[v].begin());
}
Сколько чисел меньше x
Запрос разбивается на узлов, и в каждом ответ находится двоичным поиском по отсортированному массиву. Итого на запрос.
int count = std::lower_bound(tree[v].begin(), tree[v].end(), x) - tree[v].begin();
Логарифм в квадрате при — это около обращений, что проходит. Но константа у двоичного поиска по чужой памяти большая, и на такое решение уже не годится.
Главный минус — массивы неизменяемы. Вставить элемент в отсортированный массив узла нельзя, не сдвинув половину. Дерево слияний живёт в задачах без изменений.
K-е по величине на отрезке
Первый способ — двоичный поиск по ответу поверх запроса «сколько меньше ». Получается , и это заметно медленно.
Второй способ — дерево с сохранением версий (персистентное). Идея простая: добавляя -й элемент в дерево по значениям, мы меняем только путь от корня к листу — это узлов. Остальные можно не копировать, а переиспользовать.
Тогда «какие числа лежат в » — это разность версий и : количество в любом узле считается вычитанием счётчиков двух версий. Спуск с этой разностью даёт -ю статистику за один .
int kth(int leftVersion, int rightVersion, int tl, int tr, int k) {
if (tl == tr) return tl;
int inLeft = count[left[rightVersion]] - count[left[leftVersion]];
int tm = (tl + tr) / 2;
if (k <= inLeft) return kth(left[leftVersion], left[rightVersion], tl, tm, k);
return kth(right[leftVersion], right[rightVersion], tm + 1, tr, k - inLeft);
}
Узлов всего , и хранятся они не в куче массивов, а в трёх плоских: left, right, count. Индексы вместо указателей здесь не оптимизация, а способ не утонуть в аллокациях.
Разность версий работает потому, что счётчик — величина обратимая. Для максимума такой фокус не пройдёт, и запрос про максимум на отрезке версий персистентным деревом не решается.
Что выбирать
- запросов немного, изменений нет — дерево слияний, оно пишется вдвое короче;
- запросов много или нужна именно -я статистика — персистентное дерево;
- изменения есть — ни то, ни другое; смотрите в сторону корневой декомпозиции или разбиения запросов на офлайн-группы.