EduBrick

Спуск по дереву

Вопрос «где» вместо «сколько»: k-й ноль и первый элемент не меньше x за один логарифм вместо двух.

3 мин

Половина задач спрашивает не «сколько», а «где»: где kk-й ноль, где первый элемент не меньше xx, где kk-й ещё не удалённый.

Первое, что приходит в голову, — двоичный поиск по ответу: проверяем «сколько нулей на префиксе до mm» запросом к дереву и ищем нужное mm. Работает, но стоит O(log2n)O(\log^2 n).

Спуск делает то же за один логарифм.

Спуск по аддитивной величине

Стоя в узле, мы уже знаем ответ для каждой половины. Значит выбор, куда идти, делается на месте — без нового запроса:

int kthZero(int v, int tl, int tr, int k) {
    if (tl == tr) return tl;
    int tm = (tl + tr) / 2;
    if (tree[2 * v] >= k) return kthZero(2 * v, tl, tm, k);
    return kthZero(2 * v + 1, tm + 1, tr, k - tree[2 * v]);
}

Здесь tree[v] — количество нулей на отрезке узла. Спуск идёт строго вниз, без возвратов: O(logn)O(\log n).

Так же ищется первый префикс с суммой не меньше xx — при условии, что элементы неотрицательны и префиксные суммы не убывают.

Спуск по монотонной величине

Второй вид: величина не аддитивна, зато монотонна по вложению. Максимум поддерева не меньше максимума любой его части, поэтому если максимум узла меньше xx, внутри искать нечего.

int firstAtLeast(int v, int tl, int tr, int from, int x) {
    if (tr < from || tree[v] < x) return -1;
    if (tl == tr) return tl;
    int tm = (tl + tr) / 2;
    int got = firstAtLeast(2 * v, tl, tm, from, x);
    return got != -1 ? got : firstAtLeast(2 * v + 1, tm + 1, tr, from, x);
}

Выглядит как полный обход, но две проверки в первой строке обрезают всё лишнее. Узлы, куда мы зашли и вернулись ни с чем, лежат на левой границе, а их O(logn)O(\log n).

Спуск внутри отрезка

Оба примера ищут по всему массиву. Если вопрос про отрезок [l,r][l, r], есть два способа.

Простой. Обычным запросом посчитать, сколько нулей на [l,r][l, r]; если меньше kk — ответа нет. Иначе посчитать количество нулей на префиксе до l1l - 1 и спуститься за (k+это число)(k + \text{это число})-м нулём по всему дереву. Два запроса и один спуск — те же логарифмы.

Быстрый. Написать спуск, который сам разбирается с границами: заходит в пересекающиеся узлы и вычитает по дороге. Быстрее по константе, но заметно легче ошибиться.

Начинайте с простого. Второй нужен, когда запросов миллионы.

Чем спуск отличается от двоичного поиска

Двоичный поиск задаёт дереву вопрос logn\log n раз. Спуск использует то, что дерево само устроено как двоичный поиск, и обходится одним проходом.

Это общая мысль: если структура уже содержит нужное разбиение, искать поверх неё — значит делать работу дважды.