Skip to Content

Ancestro común más bajo - algoritmo offline de Tarjan

Tenemos un árbol GG con nn nodos y tenemos mm consultas de la forma (u,v)(u, v). Para cada consulta (u,v)(u, v) queremos encontrar el ancestro común más bajo (LCA) de los vértices uu y vv, es decir, el nodo que es ancestro de uu y de vv y tiene la mayor profundidad en el árbol. El nodo vv también es un ancestro de vv, así que el LCA también puede ser uno de los dos nodos.

En este artículo resolveremos el problema de forma offline, es decir, asumimos que todas las consultas se conocen de antemano, y por tanto respondemos las consultas en el orden que queramos. El siguiente algoritmo permite responder las mm consultas en tiempo total O(n+m)O(n + m), es decir, para mm suficientemente grande en O(1)O(1) por cada consulta.

Algoritmo

El algoritmo lleva el nombre de Robert Tarjan, que lo descubrió en 1979 y también hizo muchas otras contribuciones a la estructura de datos Union-Find / conjuntos disjuntos (DSU), que se usará intensamente en este algoritmo.

El algoritmo responde todas las consultas con un solo recorrido DFS del árbol. A saber, una consulta (u,v)(u, v) se responde en el nodo uu, si el nodo vv ya ha sido visitado previamente, o viceversa.

Así que supongamos que estamos actualmente en el nodo vv, ya hemos hecho las llamadas recursivas de DFS, y también ya visitamos el segundo nodo uu de la consulta (u,v)(u, v). Aprendamos cómo encontrar el LCA de estos dos nodos.

Nótese que LCA(u,v)\text{LCA}(u, v) es o el nodo vv o uno de sus ancestros. Así que hay que encontrar el nodo más bajo entre los ancestros de vv (incluyendo vv), para el cual el nodo uu es un descendiente. También nótese que para un vv fijo los nodos visitados del árbol se parten en un conjunto de conjuntos disjuntos. Cada ancestro pp del nodo vv tiene su propio conjunto que contiene este nodo y todos los subárboles con raíces en aquellos de sus hijos que no son parte del camino de vv a la raíz del árbol. El conjunto que contiene el nodo uu determina el LCA(u,v)\text{LCA}(u, v): el LCA es el representante del conjunto, a saber el nodo que yace en el camino entre vv y la raíz del árbol.

Solo hay que aprender a mantener de forma eficiente todos estos conjuntos. Para este propósito aplicamos la estructura de datos DSU. Para poder aplicar unión por rango, guardamos el representante real (el valor en el camino entre vv y la raíz del árbol) de cada conjunto en el arreglo ancestor.

Discutamos la implementación del DFS. Supongamos que estamos visitando actualmente el nodo vv. Colocamos el nodo en un conjunto nuevo en el DSU, ancestor[v] = v. Como es usual procesamos todos los hijos de v.ParaelloprimerodebemosllamarrecursivamenteaDFSdesdeesenodo,yluegoagregarestenodocontodosusubaˊrbolalconjuntodev. Para ello primero debemos llamar recursivamente a DFS desde ese nodo, y luego agregar este nodo con todo su subárbol al conjunto de</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.0359em;">v</span><span class="mord">‘.</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">a</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">o</span><span class="mord mathnormal">p</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">im</span><span class="mord mathnormal" style="margin-right:0.0278em;">er</span><span class="mord mathnormal">o</span><span class="mord mathnormal">d</span><span class="mord mathnormal">e</span><span class="mord mathnormal">b</span><span class="mord mathnormal">e</span><span class="mord mathnormal">m</span><span class="mord mathnormal">os</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">ama</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">u</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">s</span><span class="mord mathnormal">i</span><span class="mord mathnormal" style="margin-right:0.0359em;">v</span><span class="mord mathnormal">am</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal">t</span><span class="mord mathnormal">e</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0278em;">D</span><span class="mord mathnormal" style="margin-right:0.1389em;">F</span><span class="mord mathnormal" style="margin-right:0.0576em;">S</span><span class="mord mathnormal">d</span><span class="mord mathnormal">es</span><span class="mord mathnormal">d</span><span class="mord mathnormal">eese</span><span class="mord mathnormal">n</span><span class="mord mathnormal">o</span><span class="mord mathnormal">d</span><span class="mord mathnormal">o</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal" style="margin-right:0.0359em;">y</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">u</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord mathnormal">o</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">es</span><span class="mord mathnormal">t</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal">o</span><span class="mord mathnormal">d</span><span class="mord mathnormal">oco</span><span class="mord mathnormal">n</span><span class="mord mathnormal">t</span><span class="mord mathnormal">o</span><span class="mord mathnormal">d</span><span class="mord mathnormal">os</span><span class="mord mathnormal">u</span><span class="mord mathnormal">s</span><span class="mord mathnormal">u</span><span class="mord mathnormal">b</span><span class="mord katex-accent"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.6944em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord mathnormal">a</span></span><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="accent-body" style="left:-0.25em;"><span class="mord">ˊ</span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">b</span><span class="mord mathnormal">o</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">co</span><span class="mord mathnormal" style="margin-right:0.0572em;">nj</span><span class="mord mathnormal">u</span><span class="mord mathnormal">n</span><span class="mord mathnormal">t</span><span class="mord mathnormal">o</span><span class="mord mathnormal">d</span><span class="mord mathnormal">e</span></span></span></span></span>v. Esto se puede hacer con la función union_sets y la siguiente asignación ancestor[find_set(v)] = v (esto es necesario, porque union_sets podría cambiar el representante del conjunto).

Finalmente, después de procesar todos los hijos podemos responder todas las consultas de la forma (u,v)(u, v) para las que uu ya ha sido visitado. La respuesta a la consulta, es decir, el LCA de uu y vv, será el nodo ancestor[find_set(u)]. Es fácil ver que una consulta solo se responderá una vez.

Determinemos la complejidad temporal de este algoritmo. Primero tenemos O(n)O(n) por el DFS. Segundo tenemos las llamadas a la función union_sets que ocurren nn veces, resultando también en O(n)O(n). Y tercero tenemos las llamadas a find_set por cada consulta, lo que da O(m)O(m). Así que en total la complejidad temporal es O(n+m)O(n + m), lo que significa que para mm suficientemente grande esto corresponde a O(1)O(1) para responder una consulta.

Implementación

Aquí hay una implementación de este algoritmo. La implementación del DSU no se ha incluido, ya que se puede usar sin ninguna modificación.

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() { // initialize n, adj and DSU // for (each query (u, v)) { // queries[u].push_back(v); // queries[v].push_back(u); // } ancestor.resize(n); visited.assign(n, false); dfs(0); }