Atlasingeniería

Estructuras de datosEstructuras para consultas por rangoTema 2

Á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 O(1)O(1) 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: O(logn)O(\log n) las dos.

Cuál de las tres

Sumas acumuladasÁrbol de FenwickSegment tree
Consulta por rangoO(1)O(log n)O(log n)
Modificar un elementoO(n)O(log n)O(log n)
Memorian + 1n + 1Unas 4n posiciones
Qué operaciones soportaSumas y restasOperaciones invertibles: sumas, xorCualquiera asociativa: mínimo, máximo, gcd, suma
Modificación por rangoNoCon dos árbolesSí, con propagación diferida
Cuánto código esCuatro líneasUnas quinceBastante más
El Fenwick es la respuesta por defecto cuando alcanza con sumas. El segment tree es para cuando la operación no es invertible —un mínimo no se puede «restar»— o hace falta modificar rangos enteros.

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 ii guarda la suma de un bloque que termina en ii y cuyo largo es el último bit encendido de ii:

largo(i)=i&(i).largo(i) = i \mathbin{\&} (-i).

Esa operación —el bit menos significativo que está en uno— es todo el mecanismo. Para consultar el prefijo hasta ii se suma el bloque de ii y se salta al que termina antes, apagando ese bit. Para modificar la posición ii 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.

1 / 5
Qué cubre cada posición, en un Fenwick de ocho. Las potencias de dos cubren bloques grandes; los índices impares, un solo elemento. Consultar el prefijo hasta 7 son tres lecturas: 4, 6 y 7.

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.

1 / 3
Un segment tree de mínimos sobre [5, 2, 7, 3]. Cada nodo guarda el mínimo de su mitad; la raíz, el de todo. Modificar una hoja obliga a recalcular sólo sus ancestros: la altura del árbol.

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 O(logn)O(\log n) nodos completos.

Antes de seguir, predecí

¿Por qué un segment tree sirve para mínimos y un Fenwick clásico no?

Modificar rangos: propagación diferida

«Sumale 5 a todo el rango 100..250» sobre un segment tree común cuesta tocar cada hoja: O(n)O(n). 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

  1. Cada nodo tiene, además de su valor, una marca con la modificación pendiente para todo su bloque.
  2. 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.
  3. Antes de usar un nodo —para consultar o para bajar— se empuja su marca a los dos hijos y se limpia.
  4. 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ónQué estructuraPor qué
Ranking en vivo con puntajes que cambianFenwickContar cuántos hay por debajo de un puntaje es un prefijo
Contar inversiones al ordenarFenwickSe recorre y se pregunta cuántos mayores ya pasaron
Mínimo o máximo de una ventana que se mueveSegment tree, o una cola dobleEl mínimo no es invertible
Ocupación de recursos por franja horariaSegment tree con propagación diferidaLas reservas modifican rangos enteros
Métricas por rango de tiempo que ya no cambianSumas acumuladasNo hace falta nada más caro
La última fila es el recordatorio más útil: la estructura más simple que cumple es la respuesta correcta, y muchas veces no hace falta ningún árbol.
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 O(logn)O(\log n) en vez de O(log2n)O(\log^2 n) 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?

¿Por qué no alcanzan las sumas acumuladas cuando el arreglo cambia?
¿Qué operación es el mecanismo central del árbol de Fenwick?
¿Por qué el arreglo interno de un Fenwick se indexa desde 1?
Necesitás el mínimo de rangos arbitrarios sobre datos que cambian. ¿Qué usás?
¿Qué resuelve la propagación diferida?