Ancestro común más bajo - algoritmo de Farach-Colton y Bender
Sea un árbol. Para cada consulta de la forma queremos encontrar el ancestro común más bajo de los nodos y , es decir, queremos encontrar un nodo que yace en el camino de al nodo raíz, que yace en el camino de al nodo raíz, y si hay varios nodos elegimos el que está más lejos del nodo raíz. En otras palabras, el nodo deseado es el ancestro más bajo de y . En particular, si es un ancestro de , entonces es su ancestro común más bajo.
El algoritmo que se describe en este artículo fue desarrollado por Farach-Colton y Bender. Es asintóticamente óptimo.
Algoritmo
Usamos la reducción clásica del problema de LCA al problema de RMQ. Recorremos todos los nodos del árbol con DFS y mantenemos un arreglo con todos los nodos visitados y las alturas de estos nodos. El LCA de dos nodos y es el nodo entre las ocurrencias de y en el tour que tiene la menor altura.
En la siguiente imagen se puede ver un posible tour de Euler de un grafo y en la lista de abajo se pueden ver los nodos visitados y sus alturas.
Se puede leer más sobre esta reducción en el artículo Ancestro común más bajo. En ese artículo el mínimo de un rango se encontraba o bien por descomposición por raíz cuadrada en o bien en usando un Árbol de Segmentos. En este artículo vemos cómo podemos resolver las consultas de mínimo en un rango dadas en tiempo , tomando todavía solo de tiempo para el preprocesamiento.
Nótese que el problema de RMQ reducido es muy específico: cualquier par de elementos adyacentes del arreglo difieren exactamente en uno (ya que los elementos del arreglo no son más que las alturas de los nodos visitados en el orden del recorrido, y o bien vamos a un descendiente, en cuyo caso el siguiente elemento es uno mayor, o bien volvemos al ancestro, en cuyo caso el siguiente elemento es uno menor). El algoritmo de Farach-Colton y Bender describe una solución exactamente para este problema de RMQ especializado.
Denotemos con el arreglo sobre el que queremos realizar las consultas de mínimo en un rango. Y será el tamaño de .
Hay una estructura de datos sencilla que podemos usar para resolver el problema de RMQ con preprocesamiento y por cada consulta: la Tabla Dispersa. Creamos una tabla donde cada elemento es igual al mínimo de en el intervalo . Obviamente , y por tanto el tamaño de la Tabla Dispersa será . Se puede construir la tabla fácilmente en observando que .
¿Cómo podemos responder una consulta RMQ en usando esta estructura de datos? Sea la consulta recibida , entonces la respuesta es , donde es el mayor exponente tal que no es mayor que la longitud del rango . En efecto, podemos tomar el rango y cubrirlo con dos segmentos de longitud : uno que empieza en y el otro que termina en . Estos segmentos se solapan, pero esto no interfiere con nuestro cálculo. Para lograr realmente la complejidad temporal de por consulta, necesitamos conocer los valores de para todas las longitudes posibles de a . Pero esto se puede precomputar fácilmente.
Ahora queremos mejorar la complejidad del preprocesamiento hasta .
Dividimos el arreglo en bloques de tamaño , donde es el logaritmo en base 2. Para cada bloque calculamos el elemento mínimo y lo guardamos en un arreglo . tiene tamaño . Construimos una tabla dispersa a partir del arreglo . Su tamaño y su complejidad temporal serán:
Ahora solo tenemos que aprender a responder rápidamente consultas de mínimo en un rango dentro de cada bloque. De hecho, si la consulta de mínimo en un rango recibida es y y están en bloques distintos, entonces la respuesta es el mínimo de los siguientes tres valores: el mínimo del sufijo del bloque de que empieza en , el mínimo del prefijo del bloque de que termina en , y el mínimo de los bloques entre ellos. El mínimo de los bloques intermedios se puede responder en usando la Tabla Dispersa. Así que solo nos quedan las consultas de mínimo en un rango dentro de los bloques.
Aquí aprovecharemos la propiedad del arreglo. Recordemos que los valores del arreglo — que no son más que valores de altura en el árbol — siempre diferirán en uno. Si quitamos el primer elemento de un bloque y lo restamos de cada otro elemento del bloque, cada bloque se puede identificar por una secuencia de longitud formada por los números y . Como estos bloques son tan pequeños, solo hay unas pocas secuencias distintas que pueden ocurrir. El número de secuencias posibles es:
Así, el número de bloques distintos es , y por tanto podemos precomputar los resultados de las consultas de mínimo en un rango dentro de todos los bloques distintos en tiempo . Para la implementación podemos caracterizar un bloque por una máscara de bits de longitud (que cabrá en un int estándar) y guardar el índice del mínimo en un arreglo de tamaño .
Así aprendimos a precomputar consultas de mínimo en un rango dentro de cada bloque, así como consultas de mínimo en un rango sobre un rango de bloques, todo en .
Con estas precomputaciones podemos responder cada consulta en , usando a lo sumo cuatro valores precomputados: el mínimo del bloque que contiene l, el mínimo del bloque que contiene r, y los dos mínimos de los segmentos solapados de los bloques entre ellos.
Implementación
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) {
// obtener el tour de Euler y los índices de las primeras ocurrencias
first_visit.assign(n, -1);
height.assign(n, 0);
euler_tour.reserve(2 * n);
dfs(root, -1, 0);
// precomputar todos los valores de log
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;
// precomputar el mínimo de cada bloque y construir la tabla dispersa
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]);
}
}
// precomputar la máscara de cada bloque
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);
}
// precomputar RMQ para cada bloque único
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];
}