Метод двоичных подъемов - Алгоритмика
Метод двоичных подъемов

Метод двоичных подъемов

автор Сергей Слотин
пререквизиты Связь задачи LCA и static RMQ

В контексте решения задачи LCA и не только популярен следующий метод.

Предподсчет

Предподсчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го и так далее. Сохраним их в двумерном массиве up размера $n \times \lceil \log n \rceil$: в up[v][d] будет храниться предок вершины $v$ на расстоянии $2^d$, а если такой вершины не существует, то корень.

Такой препроцессинг можно выполнить за $O(n \log n)$, используя тот факт, что предок на расстоянии $2^{d+1}$ — это предок на расстоянии $2^d$ предка на расстоянии $2^d$:

int up[maxn][logn];

void dfs(int v) {
    for (int l = 1; l < logn; l++)
        up[v][l] = up[up[v][l-1]][l-1];
    tin[v] = t++;
    for (int u : g[v]) {
        up[u][0] = v;
        dfs(u);
    }
    tout[v] = t;
}

Препроцессинг суммарно занимает $O(n \log n)$ времени и памяти, потому что таков размер массива up, и каждый его элемент вычисляется за константу.

Запрос

Пусть теперь поступил запрос нахождения LCA — пара вершин $(u, v)$:

  • Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом.
  • Иначе, пользуясь массивом up, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA.

Подробнее про второй пункт. Присвоим $i = \lceil \log n \rceil$ и будем уменьшать эту переменную на единицу, пока up[v][i] не перестанет быть предком $u$. Когда это произойдёт, подвинем указатель на $2^i$-го предка $v$ и продолжим дальше:

int lca(int v, int u) {
    if (a(v, u)) return v;
    if (a(u, v)) return u;
    for (int l = logn-1; l >= 0; l--)
        if (!ancestor(up[v][l], u))
            v = up[v][l];
    return up[v][0];
}

Указатель up[v][i] изначально является корнем дерева, а затем будет каждую итерацию спускаться на $2^i$. Когда он станет потомком lca, нам достаточно подняться всего лишь один раз, потому что два раза прыгнуть на расстояние $2^i$ это то же самое, что один раз прыгнуть на $2^{i+1}$, а мы могли это сделать на предыдущем шаге.

Ответ на произвольный запрос будет работать за $O(\log n)$, потому что мы фактически выполняем бинпоиск.

Запросы на путях

Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа).

Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером $2^d$-ого предка минимум на соответствующем пути.

Заметим, что минимум на пути от $u$ до $v$ — это минимум от минимума на пути от $u$ до $lca(u, v)$ и от минимума на пути от $v$ до $lca(u, v)$. В свою очередь, оба этих минимума — это минимум на всех двоичных подъемах до LCA.

int get_min(int v, int u) {
    int res = inf;
    for (int l = logn-1; l >= 0; l--)
        if (!ancestor(up[v][l], u))
            v = up[v][l], res = min(res, mn[v][l]);
    for (int l = logn-1; l >= 0; l--)
        if (!ancestor(up[u][l], v))
            u = up[u][l], res = min(res, mn[u][l]);
    return min({res, mn[v][0], mn[u][0]})
}

Аналогичным образом можно считать сумму, gcd, полиномиальный хеш и много других странных функций на пути, но только в статическом случае — когда у нас нет обновлений. Для динамического случая существует гораздо более сложный метод, называемый heavy-light декомпозицией.