Двоичные подъёмы и LCA
Запомнить прыжки на степени двойки — и подъём на любую высоту складывается из битов. Два способа искать наименьшего общего предка, оба за логарифм.
4 мин
Наименьший общий предок (LCA) вершин и — самая глубокая вершина, являющаяся предком обеих.
Наивно: подниматься от к корню и на каждом шаге проверять критерием через tin/tout, не стал ли текущий предок предком и для . Ответ верный, но на бамбуке это на запрос.
Идея
Выпишем предков снизу вверх. Сначала идут вершины, не являющиеся предками , потом — являющиеся, и переключение происходит ровно один раз. То есть перед нами массив вида , и нужна первая единица.
Такое ищется бинарным поиском — только массива у нас нет, по нему нельзя индексироваться. Зато можно заранее насчитать прыжки на степени двойки и набирать нужную высоту по битам, от старшего к младшему. Это экспоненциальный поиск в чистом виде.
Предподсчёт
up[k][v] — куда попадём, поднявшись из на рёбер. Считается по индукции: прыжок на — это два прыжка на .
int LOG = 1;
while ((1 << LOG) <= n) LOG++;
vector<vector<int>> up(LOG, vector<int>(n));
for (int v = 0; v < n; v++) up[0][v] = (par[v] == -1 ? v : par[v]);
for (int k = 1; k < LOG; k++)
for (int v = 0; v < n; v++)
up[k][v] = up[k - 1][up[k - 1][v]];
Корень считаем своим родителем. Тогда слишком длинный прыжок упирается в корень и остаётся там — и не надо отдельно проверять выход за границу.
Время и память — .
Способ первый: через tin/tout
Поднимаем , пока не перескочили: прыжок делаем только если он не приводит в предка .
int lca(int u, int v) {
if (isAncestor(u, v)) return u;
if (isAncestor(v, u)) return v;
for (int k = LOG - 1; k >= 0; k--)
if (!isAncestor(up[k][u], v)) u = up[k][u];
return up[0][u];
}
Две проверки в начале обязательны: если уже предок , ни одного прыжка не будет, и финальный up[0][u] уведёт на уровень выше ответа.
Способ второй: выровнять высоты
Первый способ несимметричен: одну вершину поднимаем, вторую держим. Второй способ симметричен и обходится без tin/tout — нужны только глубины.
Сначала поднимаем более глубокую вершину до уровня второй. Разность глубин раскладывается по битам:
int jump(int v, int k) {
for (int i = 0; i < LOG; i++) if (k >> i & 1) v = up[i][v];
return v;
}
Теперь вершины на одной высоте. Поднимаем обе синхронно: прыжок делаем, только если попадаем в разные вершины.
int lca(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
u = jump(u, dep[u] - dep[v]);
if (u == v) return u;
for (int k = LOG - 1; k >= 0; k--)
if (up[k][u] != up[k][v]) { u = up[k][u]; v = up[k][v]; }
return up[0][u];
}
Почему совпадение вершин означает «перелетели»: раз обе вершины на одной высоте, они одинаково удалены от своего LCA. Прыжок, перескакивающий LCA, приводит обе в одну и ту же вершину; прыжок короче — в разные.
Условие u == v после выравнивания обязательно: оно ловит случай, когда одна вершина оказалась предком другой.
Проверено: оба способа совпали с подъёмом по родителям на 20 000 деревьях, суммарно около 1,4 миллиона запросов.
Цена памяти
памяти — это не только формальность. При массив занимает МБ. Кэш последнего уровня у типичного процессора — единицы мегабайт, так что таблица в него не помещается, и обращения идут в оперативную память.
Разница между попаданием в кэш и походом в память — порядок величины. Решение не перестанет работать, но константа вырастет заметно.
Если это мешает, есть прыжковые указатели с линейной памятью — тот же логарифм на запрос, но памяти.