EduBrick

Level Ancestor и лестницы

Подняться ровно на k уровней. Один двоичный прыжок плюс обращение в массив — и запрос стоит константу.

3 мин

Задача Level Ancestor: дана вершина vv и число kk, найти предка vv, стоящего ровно на kk рёбер выше.

Двоичные подъёмы решают её сразу: раскладываем kk по битам и делаем прыжки. O(logn)O(\log n) на запрос. Но можно за O(1)O(1).

Разбиение на длиннейшие пути

Определим глубину вершины как расстояние до самого далёкого листа в её поддереве.

Разобьём дерево на вертикальные пути: начиная из корня, продолжаем путь в того сына, у которого глубина наибольшая. Остальные сыновья начинают свои пути.

void build(int v, int p, int id) {
    pathId[v] = id;
    pathPos[v] = paths[id].size();
    paths[id].push_back(v);
    int best = -1;
    for (int u : g[v]) if (u != p)
        if (best == -1 || deepest[u] > deepest[best]) best = u;
    if (best != -1) build(best, v, id);
    for (int u : g[v]) if (u != p && u != best) {
        paths.push_back({});
        build(u, v, paths.size() - 1);
    }
}

Если бы ответ всегда лежал на том же пути, что и vv, запрос сводился бы к paths[pathId[v]][pathPos[v] - k] — обращение в массив, O(1)O(1).

Но путь может кончиться раньше, чем мы поднимемся на kk. Тогда так не выйдет.

Лестницы

Приём: удлиним каждый путь вверх вдвое. Если в пути было \ell вершин, допишем в начало ещё \ell вершин-предков (или меньше, если упёрлись в корень).

Эти дописанные вершины принадлежат другим путям — они дублируются. Памяти уходит вдвое больше, зато появляется запас сверху. Такое удлинённое разбиение называется лестничной декомпозицией.

Запрос за константу

Пусть 22^\ell — наибольшая степень двойки, не превосходящая kk. Сделаем один двоичный прыжок на 22^\ell и попадём в вершину ww. Осталось подняться на k2<2k - 2^\ell < 2^\ell.

Утверждение: этот остаток гарантированно лежит внутри лестницы вершины ww.

Почему. Из ww вниз идёт путь длины хотя бы 22^\ell — по нему мы только что пришли из vv. Значит, глубина ww не меньше 22^\ell, и длиннейший путь через ww имеет длину хотя бы 22^\ell. Удлинив его вдвое, мы получили запас сверху тоже не меньше 22^\ell. А подняться осталось меньше, чем на 22^\ell.

int levelAncestor(int v, int k) {
    if (k == 0) return v;
    int l = __lg(k);
    int w = up[l][v];                       // единственный двоичный прыжок
    int rest = k - (1 << l);
    return ladders[pathId[w]][ladderPos[w] - rest];
}

Предподсчёт O(nlogn)O(n \log n) — из-за таблицы двоичных подъёмов. Запрос O(1)O(1).

Проверено: 20 000 деревьев, 765 534 запроса — ответы совпали с подъёмом по родителям, и лестницы хватало во всех случаях. Это как раз тот шаг рассуждения, который проще проверить, чем перепроверить на бумаге.

Родственная декомпозиция

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

Разница в одном слове — «глубокий» против «большого», — а применения разные.