Перейти до основного вмісту

Найнижчий спільний предок — алгоритм Фарах-Колтона–Бендера

Нехай GG — дерево. Для кожного запиту виду (u,v)(u, v) ми хочемо знайти найнижчого спільного предка вершин uu і vv, тобто хочемо знайти таку вершину ww, яка лежить на шляху від uu до кореня та на шляху від vv до кореня, а якщо таких вершин декілька, то ми обираємо ту, що найдальша від кореня. Іншими словами, шукана вершина ww — це найнижчий предок uu і vv. Зокрема, якщо uu є предком vv, то uu і є їхнім найнижчим спільним предком.

Алгоритм, який буде описано в цій статті, розробили Фарах-Колтон і Бендер. Він асимптотично оптимальний.

Коли підходить цей алгоритм?
  • Потрібні теоретично оптимальні O(1)O(1) на запит LCA при лише O(N)O(N) передобчисленні (онлайн, дуже багато запитів)? (якщо запити відомі заздалегідь, простіший вибір → офлайн Тар'ян)
  • Ви готові до складнішої реалізації заради цієї асимптотики? (якщо хочете простіший онлайн-метод і вас влаштовує O(logN)O(\log N) на запит → двійкові підйоми)
  • Задачу справді можна звести до RMQ на масиві висот ейлерового обходу, де сусідні елементи відрізняються рівно на одиницю (це ключова умова методу)?

Алгоритм

Ми скористаємося класичним зведенням задачі LCA до задачі RMQ. Ми обходимо всі вершини дерева за допомогою DFS і зберігаємо масив із усіма відвіданими вершинами та висотами цих вершин. LCA двох вершин uu і vv — це вершина між входженнями uu і vv в обході, яка має найменшу висоту.

На наступному малюнку ви можете бачити один із можливих ейлерових обходів графа, а в списку нижче — відвідані вершини та їхні висоти.

LCA_Euler_Tour
Nodes:1252621314741Heights:1232321212321\begin{array}{|l|c|c|c|c|c|c|c|c|c|c|c|c|c|} \hline \text{Nodes:} & 1 & 2 & 5 & 2 & 6 & 2 & 1 & 3 & 1 & 4 & 7 & 4 & 1 \\ \hline \text{Heights:} & 1 & 2 & 3 & 2 & 3 & 2 & 1 & 2 & 1 & 2 & 3 & 2 & 1 \\ \hline \end{array}

Докладніше про це зведення можна прочитати у статті Найнижчий спільний предок. У тій статті мінімум на відрізку знаходили або кореневим розбиттям за O(N)O(\sqrt{N}), або за O(logN)O(\log N) за допомогою дерева відрізків. У цій статті ми розглянемо, як можна розв'язувати задані запити мінімуму на відрізку за O(1)O(1), причому попередні обчислення займатимуть лише O(N)O(N).

Зауважимо, що зведена задача RMQ дуже специфічна: будь-які два сусідні елементи масиву відрізняються рівно на одиницю (адже елементи масиву — це не що інше, як висоти вершин, відвіданих у порядку обходу, і ми або спускаємося до нащадка, тоді наступний елемент на одиницю більший, або повертаємося до предка, тоді наступний елемент на одиницю менший). Алгоритм Фарах-Колтона–Бендера описує розв'язок саме цієї спеціалізованої задачі RMQ.

Позначмо через AA масив, на якому ми хочемо виконувати запити мінімуму на відрізку. А NN буде розміром AA.

Існує проста структура даних, якою ми можемо скористатися для розв'язання задачі RMQ з попередніми обчисленнями за O(NlogN)O(N \log N) і O(1)O(1) на кожен запит: розріджена таблиця. Ми створюємо таблицю TT, де кожен елемент T[i][j]T[i][j] дорівнює мінімуму AA на проміжку [i,i+2j1][i, i + 2^j - 1]. Очевидно, що 0jlogN0 \leq j \leq \lceil \log N \rceil, а отже, розмір розрідженої таблиці буде O(NlogN)O(N \log N). Цю таблицю легко побудувати за O(NlogN)O(N \log N), якщо помітити, що T[i][j]=min(T[i][j1],T[i+2j1][j1])T[i][j] = \min(T[i][j-1], T[i+2^{j-1}][j-1]).

Як же ми можемо відповісти на запит RMQ за O(1)O(1) за допомогою цієї структури даних? Нехай отриманий запит — це [l,r][l, r], тоді відповідь — min(T[l][sz],T[r2sz+1][sz])\min(T[l][\text{sz}], T[r-2^{\text{sz}}+1][\text{sz}]), де sz\text{sz} — найбільший показник степеня такий, що 2sz2^{\text{sz}} не перевищує довжини відрізка rl+1r-l+1. Дійсно, ми можемо взяти відрізок [l,r][l, r] і покрити його двома сегментами довжини 2sz2^{\text{sz}} — один починається в ll, а інший закінчується в rr. Ці сегменти перекриваються, але це не заважає нашому обчисленню. Щоб справді досягти часової складності O(1)O(1) на запит, нам потрібно знати значення sz\text{sz} для всіх можливих довжин від 11 до NN. Але це легко обчислити заздалегідь.

Тепер ми хочемо покращити складність попередніх обчислень до O(N)O(N).

Ми розбиваємо масив AA на блоки розміру K=0.5logNK = 0.5 \log N, де log\log — логарифм за основою 2. Для кожного блоку ми обчислюємо мінімальний елемент і зберігаємо їх у масиві BB. BB має розмір NK\frac{N}{K}. Ми будуємо розріджену таблицю з масиву BB. Її розмір і часова складність будуть:

NKlog(NK)=2Nlog(N)log(2Nlog(N))=\frac{N}{K}\log\left(\frac{N}{K}\right) = \frac{2N}{\log(N)} \log\left(\frac{2N}{\log(N)}\right) = =2Nlog(N)(1+log(Nlog(N)))2Nlog(N)+2N=O(N)= \frac{2N}{\log(N)} \left(1 + \log\left(\frac{N}{\log(N)}\right)\right) \leq \frac{2N}{\log(N)} + 2N = O(N)

Тепер нам залишається лише навчитися швидко відповідати на запити мінімуму на відрізку всередині кожного блоку. Насправді, якщо отриманий запит мінімуму на відрізку — це [l,r][l, r] і ll та rr лежать у різних блоках, то відповідь — це мінімум таких трьох значень: мінімум суфікса блоку ll, що починається в ll, мінімум префікса блоку rr, що закінчується в rr, і мінімум блоків між ними. Мінімум блоків між ними можна обчислити за O(1)O(1) за допомогою розрідженої таблиці. Отже, нам залишаються лише запити мінімуму на відрізку всередині блоків.

Тут ми скористаємося властивістю масиву. Пам'ятаймо, що значення в масиві — які є просто значеннями висот у дереві — завжди відрізняються на одиницю. Якщо ми приберемо перший елемент блоку і віднімемо його від кожного іншого елемента блоку, то кожен блок можна задати послідовністю довжини K1K - 1, що складається з чисел +1+1 і 1-1. Оскільки ці блоки такі маленькі, існує лише небагато різних можливих послідовностей. Кількість можливих послідовностей становить:

2K1=20.5log(N)1=0.5(2log(N))0.5=0.5N2^{K-1} = 2^{0.5 \log(N) - 1} = 0.5 \left(2^{\log(N)}\right)^{0.5} = 0.5 \sqrt{N}

Отже, кількість різних блоків — O(N)O(\sqrt{N}), а тому ми можемо обчислити заздалегідь результати запитів мінімуму на відрізку всередині всіх різних блоків за O(NK2)=O(Nlog2(N))=O(N)O(\sqrt{N} K^2) = O(\sqrt{N} \log^2(N)) = O(N). Для реалізації ми можемо охарактеризувати блок бітовою маскою довжини K1K-1 (яка вміститься у стандартний int) і зберігати індекс мінімуму в масиві block[mask][l][r]\text{block}[\text{mask}][l][r] розміру O(Nlog2(N))O(\sqrt{N} \log^2(N)).

Отже, ми навчилися обчислювати заздалегідь запити мінімуму на відрізку всередині кожного блоку, а також запити мінімуму на відрізку над діапазоном блоків — і все це за O(N)O(N). З цими попередніми обчисленнями ми можемо відповісти на кожен запит за O(1)O(1), скориставшись щонайбільше чотирма заздалегідь обчисленими значеннями: мінімумом блоку, що містить l, мінімумом блоку, що містить r, і двома мінімумами перекривних сегментів блоків між ними.

Реалізація

int n;
vector<vector<int>> adj;

int block_size, block_cnt;
vector<int> first_visit;
vector<int> euler_tour;
vector<int> height;
vector<int> log_2;
vector<vector<int>> st;
vector<vector<vector<int>>> blocks;
vector<int> block_mask;

void dfs(int v, int p, int h) {
first_visit[v] = euler_tour.size();
euler_tour.push_back(v);
height[v] = h;

for (int u : adj[v]) {
if (u == p)
continue;
dfs(u, v, h + 1);
euler_tour.push_back(v);
}
}

int min_by_h(int i, int j) {
return height[euler_tour[i]] < height[euler_tour[j]] ? i : j;
}

void precompute_lca(int root) {
// отримуємо ейлерів обхід та індекси перших входжень
first_visit.assign(n, -1);
height.assign(n, 0);
euler_tour.reserve(2 * n);
dfs(root, -1, 0);

// заздалегідь обчислюємо всі значення логарифма
int m = euler_tour.size();
log_2.reserve(m + 1);
log_2.push_back(-1);
for (int i = 1; i <= m; i++)
log_2.push_back(log_2[i / 2] + 1);

block_size = max(1, log_2[m] / 2);
block_cnt = (m + block_size - 1) / block_size;

// заздалегідь обчислюємо мінімум кожного блоку та будуємо розріджену таблицю
st.assign(block_cnt, vector<int>(log_2[block_cnt] + 1));
for (int i = 0, j = 0, b = 0; i < m; i++, j++) {
if (j == block_size)
j = 0, b++;
if (j == 0 || min_by_h(i, st[b][0]) == i)
st[b][0] = i;
}
for (int l = 1; l <= log_2[block_cnt]; l++) {
for (int i = 0; i < block_cnt; i++) {
int ni = i + (1 << (l - 1));
if (ni >= block_cnt)
st[i][l] = st[i][l-1];
else
st[i][l] = min_by_h(st[i][l-1], st[ni][l-1]);
}
}

// заздалегідь обчислюємо маску для кожного блоку
block_mask.assign(block_cnt, 0);
for (int i = 0, j = 0, b = 0; i < m; i++, j++) {
if (j == block_size)
j = 0, b++;
if (j > 0 && (i >= m || min_by_h(i - 1, i) == i - 1))
block_mask[b] += 1 << (j - 1);
}

// заздалегідь обчислюємо RMQ для кожного унікального блоку
int possibilities = 1 << (block_size - 1);
blocks.resize(possibilities);
for (int b = 0; b < block_cnt; b++) {
int mask = block_mask[b];
if (!blocks[mask].empty())
continue;
blocks[mask].assign(block_size, vector<int>(block_size));
for (int l = 0; l < block_size; l++) {
blocks[mask][l][l] = l;
for (int r = l + 1; r < block_size; r++) {
blocks[mask][l][r] = blocks[mask][l][r - 1];
if (b * block_size + r < m)
blocks[mask][l][r] = min_by_h(b * block_size + blocks[mask][l][r],
b * block_size + r) - b * block_size;
}
}
}
}

int lca_in_block(int b, int l, int r) {
return blocks[block_mask[b]][l][r] + b * block_size;
}

int lca(int v, int u) {
int l = first_visit[v];
int r = first_visit[u];
if (l > r)
swap(l, r);
int bl = l / block_size;
int br = r / block_size;
if (bl == br)
return euler_tour[lca_in_block(bl, l % block_size, r % block_size)];
int ans1 = lca_in_block(bl, l % block_size, block_size - 1);
int ans2 = lca_in_block(br, 0, r % block_size);
int ans = min_by_h(ans1, ans2);
if (bl + 1 < br) {
int l = log_2[br - bl - 1];
int ans3 = st[bl+1][l];
int ans4 = st[br - (1 << l)][l];
ans = min_by_h(ans, min_by_h(ans3, ans4));
}
return euler_tour[ans];
}

Відеоматеріали