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

Найнижчий спільний предок — O(N)O(\sqrt{N}) та O(logN)O(\log N) з попередньою обробкою за O(N)O(N)

Дано дерево GG. Дано запити виду (v1,v2)(v_1, v_2); для кожного запиту потрібно знайти найнижчий спільний предок (LCA), тобто таку вершину vv, яка лежить і на шляху від кореня до v1v_1, і на шляху від кореня до v2v_2, причому ця вершина має бути найнижчою. Іншими словами, шукана вершина vv — це найнижчий предок вершин v1v_1 і v2v_2. Очевидно, що їхній найнижчий спільний предок лежить на найкоротшому шляху між v1v_1 і v2v_2. Крім того, якщо v1v_1 є предком v2v_2, то v1v_1 і є їхнім найнижчим спільним предком.

Коли підходить цей алгоритм?
  • Запити LCA надходять онлайн (відповідати треба одразу), і ви хочете базовий підхід через зведення до RMQ на ейлеровому обході + дерево відрізків (O(logN)O(\log N)) чи кореневе розбиття (O(N)O(\sqrt{N}))?
  • Усі запити відомі заздалегідь (офлайн)? (тоді простіший і швидший → офлайн-алгоритм Тар'яна за O(1)O(1) у середньому на запит)
  • Потрібні теоретично оптимальні O(1)O(1) на запит при O(N)O(N) передобчисленні? (тоді → Фарах-Колтон–Бендер; якщо хочете простіший онлайн-метод з підйомом на kk предків → двійкові підйоми)

Ідея алгоритму

Перш ніж відповідати на запити, нам потрібно виконати попередню обробку дерева. Ми робимо обхід у глибину (DFS), починаючи з кореня, і будуємо список euler\text{euler}, який зберігає порядок вершин, які ми відвідуємо (вершина додається до списку тоді, коли ми вперше її відвідуємо, а також після повернення обходів DFS до її дітей). Це також називають ейлеровим обходом дерева. Зрозуміло, що розмір цього списку буде O(N)O(N). Нам також потрібно побудувати масив first[0..N1]\text{first}[0..N-1], який зберігає для кожної вершини ii її перше входження у euler\text{euler}. Тобто першу позицію в euler\text{euler} таку, що euler[first[i]]=i\text{euler}[\text{first}[i]] = i. Крім того, за допомогою DFS ми можемо знайти висоту кожного вузла (відстань від кореня до нього) і зберегти її в масиві height[0..N1]\text{height}[0..N-1].

То як же нам відповідати на запити за допомогою ейлерового обходу та двох додаткових масивів? Припустимо, що запит — це пара v1v_1 і v2v_2. Розглянемо вершини, які ми відвідуємо в ейлеровому обході між першим відвідуванням v1v_1 і першим відвідуванням v2v_2. Легко бачити, що LCA(v1,v2)\text{LCA}(v_1, v_2) — це вершина з найменшою висотою на цьому шляху. Ми вже зауважили, що LCA має бути частиною найкоротшого шляху між v1v_1 і v2v_2. Очевидно, що вона також має бути вершиною з найменшою висотою. А в ейлеровому обході ми, по суті, використовуємо найкоротший шлях, окрім того, що додатково відвідуємо всі піддерева, які трапляються нам на шляху. Але всі вершини в цих піддеревах розташовані нижче в дереві, ніж LCA, а отже, мають більшу висоту. Тож LCA(v1,v2)\text{LCA}(v_1, v_2) можна однозначно визначити, знайшовши вершину з найменшою висотою в ейлеровому обході між first(v1)\text{first}(v_1) і first(v2)\text{first}(v_2).

Проілюструймо цю ідею. Розглянемо такий граф і ейлерів обхід з відповідними висотами:

LCA_Euler_Tour
Vertices:1252621314741Heights:1232321212321\begin{array}{|l|c|c|c|c|c|c|c|c|c|c|c|c|c|} \hline \text{Vertices:} & 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}

В обході, що починається у вершині 66 і закінчується у 44, ми відвідуємо вершини [6,2,1,3,1,4][6, 2, 1, 3, 1, 4]. Серед цих вершин вершина 11 має найменшу висоту, тому LCA(6, 4) = 1\text{LCA(6, 4) = 1}.

Підсумуємо: щоб відповісти на запит, нам просто потрібно знайти вершину з найменшою висотою в масиві euler\text{euler} на відрізку від first[v1]\text{first}[v_1] до first[v2]\text{first}[v_2]. Отже, задача LCA зводиться до задачі RMQ (задачі знаходження мінімуму на відрізку).

Використовуючи кореневе розбиття, можна отримати розв'язок, який відповідає на кожен запит за O(N)O(\sqrt{N}) з попередньою обробкою за час O(N)O(N).

Використовуючи дерево відрізків, можна відповідати на кожен запит за O(logN)O(\log N) з попередньою обробкою за час O(N)O(N).

Оскільки збережені значення майже ніколи не оновлюватимуться, кращим вибором може бути розріджена таблиця, яка дозволяє відповідати на запит за O(1)O(1) з часом побудови O(NlogN)O(N\log N).

Реалізація

У наведеній нижче реалізації алгоритму LCA використовується дерево відрізків.

struct LCA {
vector<int> height, euler, first, segtree;
vector<bool> visited;
int n;

LCA(vector<vector<int>> &adj, int root = 0) {
n = adj.size();
height.resize(n);
first.resize(n);
euler.reserve(n * 2);
visited.assign(n, false);
dfs(adj, root);
int m = euler.size();
segtree.resize(m * 4);
build(1, 0, m - 1);
}

void dfs(vector<vector<int>> &adj, int node, int h = 0) {
visited[node] = true;
height[node] = h;
first[node] = euler.size();
euler.push_back(node);
for (auto to : adj[node]) {
if (!visited[to]) {
dfs(adj, to, h + 1);
euler.push_back(node);
}
}
}

void build(int node, int b, int e) {
if (b == e) {
segtree[node] = euler[b];
} else {
int mid = (b + e) / 2;
build(node << 1, b, mid);
build(node << 1 | 1, mid + 1, e);
int l = segtree[node << 1], r = segtree[node << 1 | 1];
segtree[node] = (height[l] < height[r]) ? l : r;
}
}

int query(int node, int b, int e, int L, int R) {
if (b > R || e < L)
return -1;
if (b >= L && e <= R)
return segtree[node];
int mid = (b + e) >> 1;

int left = query(node << 1, b, mid, L, R);
int right = query(node << 1 | 1, mid + 1, e, L, R);
if (left == -1) return right;
if (right == -1) return left;
return height[left] < height[right] ? left : right;
}

int lca(int u, int v) {
int left = first[u], right = first[v];
if (left > right)
swap(left, right);
return query(1, 0, euler.size() - 1, left, right);
}
};

Задачі для практики

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