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

Найнижчий спільний предок — офлайн-алгоритм Тар'яна для LCA

Маємо дерево GG з nn вершинами та mm запитів вигляду (u,v)(u, v). Для кожного запиту (u,v)(u, v) ми хочемо знайти найнижчого спільного предка вершин uu та vv, тобто вершину, яка є предком як uu, так і vv, і має найбільшу глибину в дереві. Вершина vv також є предком самої себе, тож LCA може бути й однією з цих двох вершин.

У цій статті ми розв'яжемо задачу офлайн, тобто вважаємо, що всі запити відомі заздалегідь, і тому можемо відповідати на них у будь-якому зручному для нас порядку. Наведений алгоритм дозволяє відповісти на всі mm запитів за сумарний час O(n+m)O(n + m), тобто для достатньо великого mm за O(1)O(1) на кожен запит.

Коли підходить цей алгоритм?
  • Усі запити LCA відомі заздалегідь (офлайн), тож можна відповідати на них у зручному порядку за один обхід дерева? (якщо запити надходять онлайн → двійкові підйоми або Фарах-Колтон–Бендер)
  • Запитів багато (mm велике), щоб амортизовані O(1)O(1) на запит дали виграш над O(logN)O(\log N)-методами?
  • Ви знайомі зі структурою СНМ, на якій ґрунтується алгоритм?

Алгоритм

Алгоритм названо на честь Роберта Тар'яна, який відкрив його у 1979 році, а також зробив багато інших внесків у структуру даних система неперетинних множин, яку ми активно використовуватимемо в цьому алгоритмі.

Алгоритм відповідає на всі запити за один обхід дерева DFS. А саме, на запит (u,v)(u, v) ми відповідаємо у вершині uu, якщо вершину vv вже було відвідано раніше, або навпаки.

Тож припустимо, що ми зараз перебуваємо у вершині vv, вже зробили рекурсивні виклики DFS, а також вже відвідали другу вершину uu із запиту (u,v)(u, v). Навчимося знаходити LCA цих двох вершин.

Зауважимо, що LCA(u,v)\text{LCA}(u, v) — це або вершина vv, або один із її предків. Отже, нам потрібно знайти найнижчу вершину серед предків vv (включно з vv), для якої вершина uu є нащадком. Зауважимо також, що для фіксованої vv відвідані вершини дерева розбиваються на сукупність неперетинних множин. Кожен предок pp вершини vv має власну множину, що містить цю вершину та всі піддерева з коренями в тих її дітях, які не належать шляху від vv до кореня дерева. Множина, яка містить вершину uu, визначає LCA(u,v)\text{LCA}(u, v): LCA — це представник множини, а саме та вершина, яка лежить на шляху між vv і коренем дерева.

Нам залишається лише навчитися ефективно підтримувати всі ці множини. Для цього ми застосовуємо структуру даних СНМ. Щоб мати змогу застосовувати об'єднання за рангом, ми зберігаємо справжнього представника (значення на шляху між vv і коренем дерева) кожної множини в масиві ancestor.

Обговорімо реалізацію DFS. Припустимо, що ми зараз відвідуємо вершину vv. Ми поміщаємо цю вершину в нову множину в СНМ: ancestor[v] = v. Як зазвичай, ми обробляємо всіх дітей vv. Для цього ми спершу маємо рекурсивно викликати DFS з тієї вершини, а потім додати цю вершину разом з усім її піддеревом до множини vv. Це можна зробити за допомогою функції union_sets і наступного присвоєння ancestor[find_set(v)] = v (воно необхідне, бо union_sets може змінити представника множини).

Нарешті, після обробки всіх дітей ми можемо відповісти на всі запити вигляду (u,v)(u, v), для яких uu вже було відвідано. Відповіддю на запит, тобто LCA вершин uu і vv, буде вершина ancestor[find_set(u)]. Легко бачити, що на кожен запит буде дано відповідь лише один раз.

Визначмо часову складність цього алгоритму. По-перше, ми маємо O(n)O(n) через DFS. По-друге, ми маємо виклики функції union_sets, яких відбувається nn разів, що також дає O(n)O(n). По-третє, ми маємо виклики find_set для кожного запиту, що дає O(m)O(m). Отже, сумарна часова складність становить O(n+m)O(n + m), а це означає, що для достатньо великого mm це відповідає O(1)O(1) на відповідь на один запит.

Реалізація

Ось реалізація цього алгоритму. Реалізацію СНМ не наведено, оскільки її можна використати без жодних змін.

vector<vector<int>> adj;
vector<vector<int>> queries;
vector<int> ancestor;
vector<bool> visited;

void dfs(int v)
{
visited[v] = true;
ancestor[v] = v;
for (int u : adj[v]) {
if (!visited[u]) {
dfs(u);
union_sets(v, u);
ancestor[find_set(v)] = v;
}
}
for (int other_node : queries[v]) {
if (visited[other_node])
cout << "LCA of " << v << " and " << other_node
<< " is " << ancestor[find_set(other_node)] << ".\n";
}
}

void compute_LCAs() {
// ініціалізуємо n, adj і СНМ
// for (кожен запит (u, v)) {
// queries[u].push_back(v);
// queries[v].push_back(u);
// }

ancestor.resize(n);
visited.assign(n, false);
dfs(0);
}