Skip to Content

Elevación binaria

Elevación binaria

HechoFuenteNombreDificultadTagsSolución
CSESCompany Queries IFácilBinary Jumpingen el módulo
Recursos
FuenteRecursoNotas
CPH18.1 - Finding Ancestors
AryanshSBinary Lifting
SecondThreadTree Basics - Binary Lifting

Explicación

La elevación binaria consiste en calcular el ancestro 2k2^k-ésimo de cada nodo para todos los valores relevantes de kk y guardarlos en una tabla.

Con esa tabla podemos responder de forma eficiente consultas sobre el ancestro kk-ésimo de todos los nodos. Esto se debe a que cualquier kk se puede descomponer en una suma de potencias de 22 usando su representación binaria.

Así, en vez de calcular de forma directa, por ejemplo, el ancestro 1313-ésimo de un nodo, podemos ir al 88-ésimo, luego al 44-ésimo y después al 11-ésimo. Esto da complejidad logarítmica para calcular el kk-ésimo padre.

Acá hay una animación de cómo saltamos, por si todavía no queda claro:

Para construir de verdad la tabla de elevación binaria, empezamos con los padres 20=12^0=1-ésimos de cada nodo, que son sus padres directos. Después pasamos a calcular los padres 21=22^1=2-ésimos, usando que el 22-ésimo padre se puede obtener como el 11-ésimo padre del 11-ésimo padre. Con la misma lógica seguimos con 222^2, 232^3, y así. Paramos cuando 2n2^n es mayor que el tamaño del árbol, porque en ese punto ya estamos con seguridad en la raíz.

Implementación

Complejidad temporal: O((N+Q)logN)\mathcal{O}((N+Q)\log N)

#include <cmath> #include <iostream> #include <vector> using std::cout; using std::endl; using std::vector; class Tree { private: const int log2dist; vector<int> par; vector<vector<int>> pow2ends; public: Tree(const vector<int> &parents) : log2dist(std::ceil(std::log2(parents.size() + 1))), par(parents.size() + 1), pow2ends(par.size(), vector<int>(log2dist + 1)) { par[0] = -1; for (int i = 0; i < parents.size(); i++) { par[i + 1] = parents[i]; } // pow2ends[n][k] guarda el padre 2^k-ésimo del nodo n // si no hay padre 2^k-ésimo, el valor es -1 for (int n = 0; n < par.size(); n++) { pow2ends[n][0] = par[n]; } for (int p = 1; p <= log2dist; p++) { for (int n = 0; n < par.size(); n++) { int halfway = pow2ends[n][p - 1]; if (halfway == -1) { pow2ends[n][p] = -1; } else { pow2ends[n][p] = pow2ends[halfway][p - 1]; } } } } /** @return el k-ésimo padre del nodo n */ int kth_parent(int n, int k) { int at = n; // descomponer k en potencias de 2 recorriendo sus bits for (int pow = 0; pow <= log2dist; pow++) { if ((k & (1 << pow)) != 0) { at = pow2ends[at][pow]; if (at == -1) { break; // parar cuando pasamos la raíz } } } return at; } }; int main() { int employee_num; int query_num; std::cin >> employee_num >> query_num; vector<int> bosses(employee_num - 1); for (int &b : bosses) { std::cin >> b; b--; } Tree tree(bosses); for (int q = 0; q < query_num; q++) { int employee; int dist; std::cin >> employee >> dist; int kth_boss = tree.kth_parent(--employee, dist); cout << (kth_boss != -1 ? kth_boss + 1 : -1) << '\n'; } }

Problemas

HechoFuenteNombreDificultadTagsSolución
CSESPlanets Queries IFácilBinary JumpingSolución
CSESPlanets Queries IINormalFunctional GraphSolución
CSESCyclic ArrayNormalBinary Jumping, Binary Search, 2PSolución
POI2010 - FrogNormalBinary Jumping, Sliding WindowSolución
CFLynyrd SkynyrdNormalSolución
Baltic OI2019 - ValleyNormal
Baltic OI2017 - TollNormalSolución
Platinum262144DifícilBinary Jumping
Baltic OI2015 - EditorMuy difícilSolución

Ancestro común más bajo

HechoFuenteNombreDificultadTagsSolución
CSESCompany Queries IIFácilLCASolución
Recursos
FuenteRecursoNotas
CPH18.3 - LCA Method 1

Descripción breve / solución

SansPapyrus683LCA Tree

Implementación alternativa

Explicación 1

Para hallar lca(a,b)\textrm{lca}(a, b), primero podemos elevar el nodo más bajo entre aa y bb hasta la misma profundidad que el otro. Después elevamos ambos nodos de forma decreciente. Al final, el padre de cualquiera de los dos es el LCA.

Implementación

Complejidad temporal: O((N+Q)logN)\mathcal{O}((N+Q)\log N)

#include <cmath> #include <iostream> #include <vector> using std::cout; using std::endl; using std::vector; class Tree { private: const int root = 0; const vector<vector<int>> &adj; const int log2dist; vector<int> par; vector<vector<int>> pow2ends; vector<int> depth; /** usar DFS para calcular las profundidades y padres de cada nodo */ void process(int at, int prev) { depth[at] = depth[prev] + 1; for (int n : adj[at]) { if (n != prev) { process(n, at); par[n] = at; } } } public: Tree(const vector<vector<int>> &adj) : adj(adj), log2dist(std::ceil(std::log2(adj.size()))), par(adj.size()), pow2ends(par.size(), vector<int>(log2dist + 1)), depth(adj.size()) { par[root] = depth[root] = -1; process(root, root); for (int n = 0; n < par.size(); n++) { pow2ends[n][0] = par[n]; } for (int p = 1; p <= log2dist; p++) { for (int n = 0; n < par.size(); n++) { int halfway = pow2ends[n][p - 1]; if (halfway == -1) { pow2ends[n][p] = -1; } else { pow2ends[n][p] = pow2ends[halfway][p - 1]; } } } } /** @return el k-ésimo padre del nodo n */ int kth_parent(int n, int k) { if (k > par.size()) { return -1; } int at = n; for (int pow = 0; pow <= log2dist; pow++) { if ((k & (1 << pow)) != 0) { at = pow2ends[at][pow]; if (at == -1) { break; } } } return at; } /** @return el LCA de los nodos n1 y n2 */ int lca(int n1, int n2) { if (depth[n1] < depth[n2]) { return lca(n2, n1); } // elevar n1 a la misma altura que n2 n1 = kth_parent(n1, depth[n1] - depth[n2]); if (n1 == n2) { return n2; // en este caso, n2 es un ancestro directo de n1 } // subir los nodos mientras no se encuentren for (int i = log2dist; i >= 0; i--) { if (pow2ends[n1][i] != pow2ends[n2][i]) { n1 = pow2ends[n1][i]; n2 = pow2ends[n2][i]; } } // en este punto, el LCA es el padre de cualquiera de los dos nodos return pow2ends[n1][0]; } }; int main() { int employee_num; int query_num; std::cin >> employee_num >> query_num; vector<vector<int>> adj(employee_num); for (int e = 1; e < employee_num; e++) { int boss; std::cin >> boss; adj[--boss].push_back(e); adj[e].push_back(boss); } Tree tree(adj); for (int q = 0; q < query_num; q++) { int e1, e2; std::cin >> e1 >> e2; cout << tree.lca(--e1, --e2) + 1 << '\n'; } }

Explicación 2

También podemos usar un tour de Euler del árbol para ayudarnos a calcular los LCA.

Sean start\texttt{start} y end\texttt{end} las tablas de tiempo de entrada y tiempo de salida de los nodos del árbol. Se llenan exactamente igual que en el módulo del tour de Euler.

Lo interesante es que, mientras llenamos start\texttt{start} y end\texttt{end}, también podemos calcular la tabla de elevación binaria de la misma forma que en la solución anterior. Esto se puede porque en un DFS estamos seguros de haber procesado todos los padres de un nodo antes que el nodo mismo, así que las tablas de cualquier ancestro de un nodo ya estarán llenas cuando lleguemos a ese nodo.

Ahora, para calcular el LCA sin las profundidades de los nodos, podemos usar que el nodo aa es ancestro del nodo bb si start[a]start[b]\texttt{start}[a] \le \texttt{start}[b] y end[b]end[a]\texttt{end}[b] \le \texttt{end}[a].

En nuestra función de LCA, primero chequeamos si un nodo ya es ancestro del otro. En ese caso, devolvemos el ancestro. Si no lo es, entonces elevamos uno de los nodos hasta que sea ancestro del otro, con un método básicamente igual al algoritmo de elevación binaria anterior. Después de eso, la respuesta es el padre del nodo que elevamos.

Implementación

Complejidad temporal: O((N+Q)logN)\mathcal{O}((N+Q)\log N)

#include <cmath> #include <iostream> #include <vector> using std::cout; using std::endl; using std::vector; class Tree { private: const int root = 0; const vector<vector<int>> &adj; const int log2dist; vector<vector<int>> pow2ends; vector<int> start, end; int timer = 0; void process(int at, int prev) { pow2ends[at][0] = prev; for (int p = 1; p <= log2dist; p++) { int halfway = pow2ends[at][p - 1]; if (halfway == -1) { pow2ends[at][p] = -1; } else { pow2ends[at][p] = pow2ends[halfway][p - 1]; } } start[at] = timer++; for (int n : adj[at]) { if (n != prev) { process(n, at); } } end[at] = timer; } public: Tree(const vector<vector<int>> &adj) : adj(adj), log2dist(std::ceil(std::log2(adj.size()))), pow2ends(adj.size(), vector<int>(log2dist + 1)), start(adj.size()), end(adj.size()) { process(root, -1); } bool is_ancestor(int n1, int n2) { return start[n1] <= start[n2] && end[n2] <= end[n1]; } int lca(int n1, int n2) { if (is_ancestor(n1, n2)) { return n1; } if (is_ancestor(n2, n1)) { return n2; } for (int i = log2dist; i >= 0; i--) { if (pow2ends[n1][i] != -1 && !is_ancestor(pow2ends[n1][i], n2)) { n1 = pow2ends[n1][i]; } } return pow2ends[n1][0]; } }; int main() { int employee_num; int query_num; std::cin >> employee_num >> query_num; vector<vector<int>> adj(employee_num); for (int e = 1; e < employee_num; e++) { int boss; std::cin >> boss; adj[--boss].push_back(e); adj[e].push_back(boss); } Tree tree(adj); for (int q = 0; q < query_num; q++) { int e1, e2; std::cin >> e1 >> e2; cout << tree.lca(--e1, --e2) + 1 << '\n'; } }

Explicación 3

También podemos hallar el LCA de dos nodos con el algoritmo LCA offline de Tarjan.

Aprovechando el recorrido DFS, podemos precomputar las respuestas a las consultas formando subárboles y calculando el padre común con una estructura similar a Union-Find / conjuntos disjuntos.

Implementación

#include <bits/stdc++.h> using namespace std; const int MAX = 2e5 + 1; bool vis[MAX]; int lca[MAX], fa[MAX]; vector<array<int, 2>> adj[MAX], qry[MAX]; int find(int u) { return (fa[u] == u) ? u : fa[u] = find(fa[u]); } void tarjan(int node) { vis[node] = true; for (auto [nxt, id] : adj[node]) { if (vis[nxt]) { continue; } tarjan(nxt); fa[nxt] = node; } for (auto &[nxt, id] : qry[node]) { if (vis[nxt]) { lca[id] = find(nxt); } } } int main() { int n, m; cin >> n >> m; iota(fa, fa + n + 1, 0); for (int i = 2; i <= n; i++) { int a; cin >> a; adj[i].push_back({a, i}); adj[a].push_back({i, i}); } for (int i = 0; i < m; i++) { int a, b; cin >> a >> b; qry[a].push_back({b, i}); qry[b].push_back({a, i}); } tarjan(1); for (int i = 0; i < m; i++) { cout << lca[i] << "\n"; } }
Recursos
FuenteRecursoNotas
cp-algoLCA with Binary Lifting
Mejoras
HechoFuenteNombreDificultadTagsSolución
CSESDistance QueriesFácilLCAen el módulo

Explicación

Como tenemos la profundidad de todos los nodos, la distancia entre dos nodos aa y bb es depth[a]+depth[b]2depth[lca(a,b)]\texttt{depth}[a] + \texttt{depth}[b] - 2 \cdot \texttt{depth}[\textrm{lca}(a, b)].

Acá hay algo de intuición por si no queda claro cómo funciona esta fórmula. Para ir del nodo aa al bb, una forma sería subir a la raíz del árbol y después bajar a bb. Eso da una distancia de depth[a]+depth[b]\texttt{depth}[a] + \texttt{depth}[b]. Sin embargo, observemos que estamos pasando por todos los nodos por encima del LCA dos veces. Por eso hay que restar dos veces la profundidad del LCA, y obtenemos la expresión final.

Implementación

Complejidad temporal: O((N+Q)logN)\mathcal{O}((N+Q)\log N)

#include <cmath> #include <iostream> #include <vector> using std::cout; using std::endl; using std::vector; // BeginCodeSnip{LCA Tree} class Tree { private: const int root = 0; const vector<vector<int>> &adj; const int log2dist; vector<int> par; vector<vector<int>> pow2ends; vector<int> depth; void process(int at, int prev) { depth[at] = depth[prev] + 1; for (int n : adj[at]) { if (n != prev) { process(n, at); par[n] = at; } } } public: Tree(const vector<vector<int>> &adj) : adj(adj), log2dist(std::ceil(std::log2(adj.size()))), par(adj.size()), pow2ends(par.size(), vector<int>(log2dist + 1)), depth(adj.size()) { par[root] = depth[root] = -1; process(root, root); for (int n = 0; n < par.size(); n++) { pow2ends[n][0] = par[n]; } for (int p = 1; p <= log2dist; p++) { for (int n = 0; n < par.size(); n++) { int halfway = pow2ends[n][p - 1]; if (halfway == -1) { pow2ends[n][p] = -1; } else { pow2ends[n][p] = pow2ends[halfway][p - 1]; } } } } int kth_parent(int n, int k) { if (k > par.size()) { return -1; } int at = n; for (int pow = 0; pow <= log2dist; pow++) { if ((k & (1 << pow)) != 0) { at = pow2ends[at][pow]; if (at == -1) { break; } } } return at; } int lca(int n1, int n2) { if (depth[n1] < depth[n2]) { return lca(n2, n1); } n1 = kth_parent(n1, depth[n1] - depth[n2]); if (n1 == n2) { return n2; } for (int i = log2dist; i >= 0; i--) { if (pow2ends[n1][i] != pow2ends[n2][i]) { n1 = pow2ends[n1][i]; n2 = pow2ends[n2][i]; } } return pow2ends[n1][0]; } // EndCodeSnip /** @return la distancia entre los nodos n1 y n2 */ int distance(int n1, int n2) { return depth[n1] + depth[n2] - depth[lca(n1, n2)] * 2; } }; int main() { std::ios::sync_with_stdio(false); std::cin.tie(NULL); int node_num; int query_num; std::cin >> node_num >> query_num; vector<vector<int>> adj(node_num); for (int e = 0; e < node_num - 1; e++) { int a, b; std::cin >> a >> b; adj[--a].push_back(--b); adj[b].push_back(a); } Tree tree(adj); for (int q = 0; q < query_num; q++) { int n1, n2; std::cin >> n1 >> n2; cout << tree.distance(--n1, --n2) << '\n'; } }

Problemas

USACO

HechoFuenteNombreDificultadTagsSolución
PlatinumMax FlowFácilLCASolución
PlatinumDisruptionNormalLCASolución
Old GoldRunning Away From the BarnNormalSmall to Large, Binary Jumping, Euler Tour
PlatinumTree BoxesDifícilLCASolución
PlatinumNew BarnsDifícilDiameterSolución
PlatinumGatheringDifícilLCA
PlatinumExercise RouteMuy difícilLCA

Generales

HechoFuenteNombreDificultadTagsSolución
CFSloth NaptimeFácilBinary JumpingSolución
CFDuff in the ArmyNormalLCASolución
Baltic OI2017 - RailwayNormalSolución
CFMST for Each EdgeNormalLCASolución
CFOmsk Metro (hard version)NormalLCASolución
CSARoot LCA QueriesNormalLCASolución
CFTwo PathsNormalLCA
Back to SchoolHot & ColdNormalLCASolución
Google KickstartDependent EventsDifícilLCA, Binary Jumping, DFSSolución
CFDouble TreeDifícilLCA, Binary Jumping
TLXFunctional ConstraintDifícilLCA
TLXGraph & DestinationDifícilLCA