Спуск по дереву
Вопрос «где» вместо «сколько»: k-й ноль и первый элемент не меньше x за один логарифм вместо двух.
3 мин
Половина задач спрашивает не «сколько», а «где»: где -й ноль, где первый элемент не меньше , где -й ещё не удалённый.
Первое, что приходит в голову, — двоичный поиск по ответу: проверяем «сколько нулей на префиксе до » запросом к дереву и ищем нужное . Работает, но стоит .
Спуск делает то же за один логарифм.
Спуск по аддитивной величине
Стоя в узле, мы уже знаем ответ для каждой половины. Значит выбор, куда идти, делается на месте — без нового запроса:
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] — количество нулей на отрезке узла. Спуск идёт строго вниз, без возвратов: .
Так же ищется первый префикс с суммой не меньше — при условии, что элементы неотрицательны и префиксные суммы не убывают.
Спуск по монотонной величине
Второй вид: величина не аддитивна, зато монотонна по вложению. Максимум поддерева не меньше максимума любой его части, поэтому если максимум узла меньше , внутри искать нечего.
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);
}
Выглядит как полный обход, но две проверки в первой строке обрезают всё лишнее. Узлы, куда мы зашли и вернулись ни с чем, лежат на левой границе, а их .
Спуск внутри отрезка
Оба примера ищут по всему массиву. Если вопрос про отрезок , есть два способа.
Простой. Обычным запросом посчитать, сколько нулей на ; если меньше — ответа нет. Иначе посчитать количество нулей на префиксе до и спуститься за -м нулём по всему дереву. Два запроса и один спуск — те же логарифмы.
Быстрый. Написать спуск, который сам разбирается с границами: заходит в пересекающиеся узлы и вычитает по дороге. Быстрее по константе, но заметно легче ошибиться.
Начинайте с простого. Второй нужен, когда запросов миллионы.
Чем спуск отличается от двоичного поиска
Двоичный поиск задаёт дереву вопрос раз. Спуск использует то, что дерево само устроено как двоичный поиск, и обходится одним проходом.
Это общая мысль: если структура уже содержит нужное разбиение, искать поверх неё — значит делать работу дважды.