Árbol Fenwick (Binary Indexed Tree) para Búsqueda Binaria
Aprende a construir, actualizar y consultar un árbol Fenwick y descubre cómo usarlo para encontrar el k‑ésimo elemento en tiempo logarítmico con Python.
Introducción
El Árbol Fenwick, también conocido como Binary Indexed Tree (BIT), es una estructura de datos compacta que permite:
- Obtener la suma de un prefijo
O(log n). - Actualizar un elemento individual en
O(log n).
Gracias a su representación basada en índices de bits, el BIT ocupa O(n) espacio y es extremadamente rápido en la práctica. Además, con una ligera variante podemos realizar búsqueda binaria sobre el árbol para localizar el índice cuyo prefijo alcanza un valor dado (útil para encontrar el k‑ésimo elemento).
Conceptos básicos del BIT
Un BIT se representa como un arreglo bit[1..n] (índices 1‑based). Cada posición i almacena la suma de un rango de elementos cuyo tamaño está determinado por el bit menos significativo de i:
bit[i] = sum( a[i - lowbit(i) + 1 .. i] )
lowbit(i) = i & -i
Con esta definición, las operaciones principales son:
- add(i, delta): incrementa
a[i]endeltay actualiza los nodos ancestros. - prefix_sum(i): devuelve
∑_{k=1}^{i} a[k]recorriendo los nodos descendentes.
Ambas se implementan con bucles que manipulan índices mediante i += lowbit(i) o i -= lowbit(i).
Implementación en Python (versión 3.9+)
La siguiente clase FenwickTree contiene los métodos esenciales, además de la funcionalidad de búsqueda binaria para encontrar el índice con una suma prefijo dada.
class FenwickTree:
"""Binary Indexed Tree (Fenwick) con búsqueda binaria.
Parámetros
----------
size: int
Número de elementos (el árbol es 1‑based).
"""
def __init__(self, size: int):
self.n = size
self.bit = [0] * (self.n + 1) # índice 0 sin uso
# ---------- Operaciones básicas ----------
def _lowbit(self, i: int) -> int:
return i & -i
def add(self, idx: int, delta: int) -> None:
"""Incrementa a[idx] en *delta*.
Complejidad: O(log n).
"""
i = idx
while i <= self.n:
self.bit[i] += delta
i += self._lowbit(i)
def prefix_sum(self, idx: int) -> int:
"""Devuelve la suma de a[1..idx].
Complejidad: O(log n).
"""
res = 0
i = idx
while i > 0:
res += self.bit[i]
i -= self._lowbit(i)
return res
def range_sum(self, left: int, right: int) -> int:
"""Suma en el rango [left, right]."""
return self.prefix_sum(right) - self.prefix_sum(left - 1)
# ---------- Búsqueda binaria sobre el BIT ----------
def find_kth(self, k: int) -> int:
"""Devuelve el menor índice *pos* tal que prefix_sum(pos) >= k.
Si k > total_sum() se devuelve -1.
Complejidad: O(log n).
"""
if k <= 0:
raise ValueError("k debe ser mayor que 0")
total = self.prefix_sum(self.n)
if k > total:
return -1
idx = 0
bit_mask = 1 << (self.n.bit_length() - 1) # mayor potencia de 2 <= n
while bit_mask:
next_idx = idx + bit_mask
if next_idx <= self.n and self.bit[next_idx] < k:
k -= self.bit[next_idx]
idx = next_idx
bit_mask >>= 1
return idx + 1
def total_sum(self) -> int:
return self.prefix_sum(self.n)
# ---------- Construcción a partir de una lista ----------
@classmethod
def from_list(cls, arr: list[int]):
"""Construye el BIT en O(n) a partir de *arr* (0‑based)."""
ft = cls(len(arr))
# Copia directa
for i, val in enumerate(arr, start=1):
ft.bit[i] = val
# Acumula los valores de los padres
for i in range(1, ft.n + 1):
j = i + ft._lowbit(i)
if j <= ft.n:
ft.bit[j] += ft.bit[i]
return ft
La clase incluye:
- Construcción O(n) mediante
from_list. - Actualizaciones y consultas O(log n).
- Función
find_kthque realiza una búsqueda binaria interna.
Ejemplos prácticos
A continuación se muestra cómo usar la clase para los casos de uso más habituales.
1️⃣ Construir a partir de una lista y consultar rangos
arr = [3, 2, -1, 6, 5, 4, 7]
bit = FenwickTree.from_list(arr)
print("Suma total:", bit.total_sum()) # 26
print("Suma[2..5]:", bit.range_sum(2, 5)) # 12 (2 + -1 + 6 + 5)
2️⃣ Actualizar valores
# Incrementar el elemento en la posición 4 en +3
bit.add(4, 3)
print("Nuevo total:", bit.total_sum()) # 29
print("Nuevo rango[2..5]:", bit.range_sum(2,5)) # 15
3️⃣ Búsqueda del k‑ésimo elemento (orden estadístico)
# Supongamos que el BIT almacena frecuencias de valores
freq = [0, 5, 2, 0, 3, 1] # índices 1..5
bit_freq = FenwickTree.from_list(freq)
for k in range(1, bit_freq.total_sum()+1):
idx = bit_freq.find_kth(k)
print(f"k={k} → índice {idx}")
# Salida: 1,1,1,1,1,2,2,4,4,4,5 (según frecuencias)
4️⃣ Aplicación: “Orden de llegada” en un juego multijugador
Imagina que cada jugador tiene una puntuación y queremos saber quién ocupa la posición k en tiempo real.
# Puntuaciones iniciales (ordenadas por ID)
points = [0, 10, 20, 15, 5, 30]
bit_pts = FenwickTree.from_list(points)
# Un jugador mejora su puntuación
player_id = 3
bit_pts.add(player_id, 7) # +7 puntos
# Consultar quién está en la posición 2 (k‑ésimo mayor)
# Necesitamos un BIT de frecuencias de puntajes, no de sumas.
# Aquí solo ilustramos la idea.
Fenwick vs. Segment Tree: ¿Cuál elegir?
| Característica | Fenwick Tree (BIT) | Segment Tree |
|---|---|---|
| Espacio | O(n) | O(4n) (aprox.) |
| Construcción | O(n) (desde lista) | O(n) |
| Actualización puntual | O(log n) | O(log n) |
| Consulta de rango | O(log n) (prefijo + resta) | O(log n) |
| Rangos con lazy propagation | No soportado nativamente | Sí |
| Operaciones personalizadas (mín, máximo, GCD) | Limitado a suma | Fácilmente extensible |
| Simplicidad de código | Muy alta | Media‑alta |
| Cache‑friendliness | Excelente (acceso lineal) | Peor (acceso recursivo) |
En la práctica, para consultas de suma y actualizaciones puntuales el BIT suele ser más rápido y consume menos memoria. Si necesitas actualizaciones de rango o operaciones distintas a la suma, el Segment Tree con lazy propagation es la opción adecuada.
Optimización, buenas prácticas y troubleshooting
🔧 Evita los errores comunes
- Índices 1‑based: El BIT no funciona con índices 0. Siempre inicializa con
size+1o utiliza una capa de adaptación. - Overflow de enteros: En Python los enteros son arbitrarios, pero si tu aplicación usa
numpy.int32verifica posibles desbordes. - Valor k fuera de rango:
find_kthdevuelve-1siksupera la suma total; maneja este caso antes de usar el índice.
⚙️ Mejores prácticas de rendimiento
- Construcción O(n): Usa
from_listen vez de insertar elemento a elemento (add) cuando ya dispones del arreglo completo. - Acceso contiguo: Mantén el BIT en una lista de Python nativa (no en
listdenumpy) para aprovechar la caché de CPU. - Batch updates: Si necesitas aplicar muchas actualizaciones seguidas, agrúpalas y reconstruye el BIT con
from_listpara lograrO(n)en vez deO(m log n).
🛡️ Seguridad y robustez
El BIT es una estructura puramente algorítmica, sin interacción directa con recursos externos, por lo que los riesgos de seguridad son mínimos. No obstante, si lo integras en una API pública:
- Valida siempre los índices y los valores de
kpara evitar excepciones inesperadas. - Limita el tamaño máximo del árbol (por ejemplo,
10^7) para evitar consumos excesivos de memoria.
Compatibilidad, rendimiento y escalabilidad
La implementación presentada es compatible con:
- Python 3.8+ (type hints sin
from __future__ import annotations). - Entornos CPython, PyPy (PyPy obtiene ~2× velocidad en bucles intensivos).
- Plataformas Windows, Linux y macOS.
Para datasets masivos (≥ 10⁸ elementos) considera:
- Usar
array('L')` onumpy.ndarrayde tipouint64para reducir la huella de memoria. - Dividir el rango en varios BITs (fenwick forest) y procesar en paralelo con
concurrent.futures.
Conclusión
El árbol Fenwick es una herramienta poderosa y ligera para consultas y actualizaciones de prefijos, y con la técnica de búsqueda binaria interna (find_kth) se extiende a problemas de orden estadístico como el k‑ésimo elemento o la selección por frecuencia. Su simplicidad lo convierte en la opción predilecta cuando la operación principal es la suma de rangos y se requiere alta velocidad de ejecución.
Domina la API mostrada, experimenta con casos de uso reales (ranking, compresión de datos, contadores de frecuencia) y, cuando tus necesidades evolucionen, evalúa la migración a estructuras más complejas como Segment Trees o Fenwick Trees de dos dimensiones.
Árbol Fenwick (Binary Indexed Tree) para Búsqueda Binaria en Python