Árbol de Fenwick y segment tree
Cuando los datos cambian tanto como se consultan, las sumas acumuladas no alcanzan. Estas dos estructuras hacen consulta y modificación en tiempo logarítmico: el Fenwick es compacto y sirve para sumas, el segment tree es más grande y sirve para casi cualquier operación.
Para este tema conviene tener claro:Sumas acumuladas y consultas por rangoÁrboles binarios y recorridos
Las sumas acumuladas resuelven las consultas por rango en con una condición: que los datos no cambien. Cuando cambian, cada modificación obliga a recalcular todo lo que sigue y la ventaja desaparece.
La salida es guardar sumas parciales en forma de árbol, en vez de sumas totales en forma de arreglo. Ninguna posición guarda el total hasta ella; cada una guarda el total de un bloque. Así, modificar toca unos pocos bloques y consultar combina unos pocos bloques: las dos.
Cuál de las tres
| Sumas acumuladas | Árbol de Fenwick | Segment tree | |
|---|---|---|---|
| Consulta por rango | O(1) | O(log n) | O(log n) |
| Modificar un elemento | O(n) | O(log n) | O(log n) |
| Memoria | n + 1 | n + 1 | Unas 4n posiciones |
| Qué operaciones soporta | Sumas y restas | Operaciones invertibles: sumas, xor | Cualquiera asociativa: mínimo, máximo, gcd, suma |
| Modificación por rango | No | Con dos árboles | Sí, con propagación diferida |
| Cuánto código es | Cuatro líneas | Unas quince | Bastante más |
Fenwick: el arreglo que se lee en binario
El árbol de Fenwick vive dentro de un arreglo común, indexado desde 1. Cada posición guarda la suma de un bloque que termina en y cuyo largo es el último bit encendido de :
Esa operación —el bit menos significativo que está en uno— es todo el mecanismo. Para consultar el prefijo hasta se suma el bloque de y se salta al que termina antes, apagando ese bit. Para modificar la posición se actualiza su bloque y se salta al bloque que lo contiene, sumando ese bit.
class FenwickTree {
private readonly sums: number[];
constructor(size: number) {
this.sums = new Array<number>(size + 1).fill(0);
}
/** Suma delta a la posición index, contada desde cero. */
add(index: number, delta: number): void {
for (let cursor = index + 1; cursor < this.sums.length; cursor += cursor & -cursor) {
this.sums[cursor] += delta;
}
}
/** Suma de las posiciones 0 hasta index, inclusive. */
prefixSum(index: number): number {
let total = 0;
for (let cursor = index + 1; cursor > 0; cursor -= cursor & -cursor) {
total += this.sums[cursor];
}
return total;
}
rangeSum(from: number, to: number): number {
return this.prefixSum(to) - (from === 0 ? 0 : this.prefixSum(from - 1));
}
}El arreglo interno, indexado desde 1. Debajo, qué rango cubre cada posición.
Segment tree: cada nodo es un bloque
El segment tree es más literal: la raíz cubre el arreglo entero, cada nodo parte su rango en dos mitades y las hojas son los elementos. Cada nodo guarda el resultado de la operación sobre su bloque.
Las hojas son los elementos del arreglo.
La consulta se apoya en una idea sola: si el rango del nodo entra entero en el rango consultado, se devuelve su valor sin bajar más. Un rango cualquiera se descompone en nodos completos.
Antes de seguir, predecí
Modificar rangos: propagación diferida
«Sumale 5 a todo el rango 100..250» sobre un segment tree común cuesta tocar cada hoja: . La propagación diferida arregla eso con una idea perezosa: anotar la modificación en el nodo que cubre el rango y no bajarla hasta que alguien pase por ahí.
Cómo funciona
- Cada nodo tiene, además de su valor, una marca con la modificación pendiente para todo su bloque.
- Al modificar un rango, se baja hasta los nodos que quedan enteros adentro y se les deja la marca. No se toca a sus hijos.
- Antes de usar un nodo —para consultar o para bajar— se empuja su marca a los dos hijos y se limpia.
- El valor del nodo se corrige con la marca en el momento de leerlo, lo que exige saber cuántos elementos cubre el bloque.
Dónde aparece esto fuera de los ejercicios
| Situación | Qué estructura | Por qué |
|---|---|---|
| Ranking en vivo con puntajes que cambian | Fenwick | Contar cuántos hay por debajo de un puntaje es un prefijo |
| Contar inversiones al ordenar | Fenwick | Se recorre y se pregunta cuántos mayores ya pasaron |
| Mínimo o máximo de una ventana que se mueve | Segment tree, o una cola doble | El mínimo no es invertible |
| Ocupación de recursos por franja horaria | Segment tree con propagación diferida | Las reservas modifican rangos enteros |
| Métricas por rango de tiempo que ya no cambian | Sumas acumuladas | No hace falta nada más caro |
Más a fondo · nivel seniorEl Fenwick también busca, no sólo suma
Hay una operación que el Fenwick hace casi gratis y que se olvida: encontrar la posición más chica cuyo prefijo supera un valor dado. Se recorre el árbol desde la potencia de dos más grande hacia abajo, avanzando cuando el bloque entra, y sale en en vez de que costaría una búsqueda binaria sobre consultas de prefijo.
Sirve directo para dos cosas frecuentes: elegir un elemento al azar con pesos que cambian, y responder «¿cuál es el k-ésimo elemento del conjunto?» cuando los elementos se insertan y borran.
Cierre
Autoevaluación
¿Lo entendiste?
Práctica