Árbol de Fenwick (BIT)
Sea alguna operación de grupo (una función binaria asociativa sobre un conjunto con elemento identidad y elementos inversos) y un arreglo de enteros de longitud . Denotamos la notación infija de como ; es decir, y para enteros arbitrarios . (Como es asociativa, omitiremos los paréntesis para el orden de aplicación de cuando usemos notación infija.)
El Árbol de Fenwick es una estructura de datos que:
- calcula el valor de la función en el rango dado (es decir, ) en tiempo
- actualiza el valor de un elemento de en tiempo
- requiere de memoria (la misma cantidad que requiere )
- es fácil de usar y de programar, especialmente en el caso de arreglos multidimensionales
La aplicación más común de un Árbol de Fenwick es calcular la suma de un rango. Por ejemplo, usando la suma sobre el conjunto de los enteros como operación de grupo, es decir : la operación binaria, , es en este caso, así que .
El Árbol de Fenwick también se llama Binary Indexed Tree (BIT). Fue descrito por primera vez en un artículo titulado “A new data structure for cumulative frequency tables” (Peter M. Fenwick, 1994).
Descripción
Visión general
Por simplicidad, asumiremos que la función está definida como sobre los enteros.
Supongamos que nos dan un arreglo de enteros, . (Nótese que usamos indexación desde cero.) Un Árbol de Fenwick es simplemente un arreglo, , donde cada elemento es igual a la suma de elementos de en algún rango, :
donde es alguna función que cumple . Definiremos en los próximos párrafos.
La estructura de datos se llama árbol porque existe una representación agradable en forma de árbol, aunque no necesitamos modelar un árbol real con nodos y aristas. Solo necesitamos mantener el arreglo para atender todas las consultas.
Nota: El Árbol de Fenwick que presentamos aquí usa indexación desde cero. Muchas personas usan una versión del Árbol de Fenwick con indexación desde uno. Por eso, en la sección de implementación también hay una implementación alternativa que usa indexación desde uno. Ambas versiones son equivalentes en complejidad temporal y espacial.
Ahora podemos escribir algo de pseudocódigo para las dos operaciones mencionadas arriba. Abajo, obtenemos la suma de elementos de en el rango y actualizamos (incrementamos) algún elemento :
def sum(int r):
res = 0
while (r >= 0):
res += t[r]
r = g(r) - 1
return res
def increase(int i, int delta):
for all j with g(j) <= i <= j:
t[j] += deltaLa función sum funciona de la siguiente manera:
- Primero, añade la suma del rango (es decir, ) al
result. - Luego, “salta” al rango y añade la suma de este rango al
result. - Esto continúa hasta que “salta” de a ; ahí es donde la función
sumdeja de saltar.
La función increase funciona con la misma analogía, pero “salta” en la dirección de índices crecientes:
- La suma de cada rango de la forma que cumple la condición se incrementa en
delta; es decir,t[j] += delta. Por lo tanto, actualiza todos los elementos de que corresponden a rangos en los que está .
La complejidad de sum y de increase depende de la función .
Hay muchas formas de elegir la función tal que para todo .
Por ejemplo, funciona la función , que da (en cuyo caso, las consultas de suma son lentas).
También podríamos tomar la función .
Esto correspondería a arreglos de sumas de prefijos (en cuyo caso, encontrar la suma del rango solo toma tiempo constante; sin embargo, las actualizaciones son lentas).
La parte ingeniosa del algoritmo de los Árboles de Fenwick es cómo usa una definición especial de la función que puede manejar ambas operaciones en tiempo .
Definición de { data-toc-label=‘Definición de ’ }
El cálculo de se define usando la siguiente operación simple: reemplazamos todos los bits finales en la representación binaria de por bits .
En otras palabras, si el dígito menos significativo de en binario es , entonces . Y en caso contrario el dígito menos significativo es un , y tomamos este y todos los demás s finales y los invertimos.
Por ejemplo obtenemos
Existe una implementación simple usando operaciones de bits para la operación no trivial descrita arriba:
donde es el operador AND bit a bit. No es difícil convencerse de que esta solución hace lo mismo que la operación descrita arriba.
Ahora, solo necesitamos encontrar una forma de iterar sobre todos los tales que .
Es fácil ver que podemos encontrar todos esos empezando desde y activando el último bit no activado. Llamaremos a esta operación . Por ejemplo, para tenemos:
Como era de esperar, también existe una forma simple de calcular usando operaciones de bits:
donde es el operador OR bit a bit.
La siguiente imagen muestra una posible interpretación del Árbol de Fenwick como árbol. Los nodos del árbol muestran los rangos que cubren.
Implementación
Suma en un arreglo unidimensional
Aquí presentamos una implementación del Árbol de Fenwick para consultas de suma y actualizaciones de un solo elemento.
El Árbol de Fenwick normal solo puede responder consultas de suma del tipo usando sum(int r), pero también podemos responder otras consultas del tipo calculando dos sumas y y restándolas.
Esto se maneja en el método sum(int l, int r).
Además, esta implementación soporta dos constructores. Se puede crear un Árbol de Fenwick inicializado con ceros, o se puede convertir un arreglo existente a la forma de Fenwick.
struct FenwickTree {
vector<int> bit; // Árbol de Fenwick (BIT)
int n;
FenwickTree(int n) {
this->n = n;
bit.assign(n, 0);
}
FenwickTree(vector<int> const &a) : FenwickTree(a.size()) {
for (size_t i = 0; i < a.size(); i++)
add(i, a[i]);
}
int sum(int r) {
int ret = 0;
for (; r >= 0; r = (r & (r + 1)) - 1)
ret += bit[r];
return ret;
}
int sum(int l, int r) {
return sum(r) - sum(l - 1);
}
void add(int idx, int delta) {
for (; idx < n; idx = idx | (idx + 1))
bit[idx] += delta;
}
};Construcción lineal
La implementación de arriba requiere tiempo . Es posible mejorarlo a tiempo .
La idea es que el número en el índice contribuirá al rango guardado en , y a todos los rangos a los que contribuye el índice . Así, al ir sumando los números en orden, solo hay que empujar la suma actual hacia el siguiente rango, donde después se empujará hacia el siguiente rango, y así sucesivamente.
FenwickTree(vector<int> const &a) : FenwickTree(a.size()){
for (int i = 0; i < n; i++) {
bit[i] += a[i];
int r = i | (i + 1);
if (r < n) bit[r] += bit[i];
}
}Mínimo de en un arreglo unidimensional { data-toc-label=‘Mínimo de en un arreglo unidimensional’ }
Es evidente que no hay una forma fácil de encontrar el mínimo del rango usando un Árbol de Fenwick, ya que el Árbol de Fenwick solo puede responder consultas del tipo .
Además, cada vez que se hace update de un valor, el nuevo valor tiene que ser menor que el valor actual.
Ambas limitaciones importantes se deben a que la operación junto con el conjunto de los enteros no forma un grupo, porque no hay elementos inversos.
struct FenwickTreeMin {
vector<int> bit;
int n;
const int INF = (int)1e9;
FenwickTreeMin(int n) {
this->n = n;
bit.assign(n, INF);
}
FenwickTreeMin(vector<int> a) : FenwickTreeMin(a.size()) {
for (size_t i = 0; i < a.size(); i++)
update(i, a[i]);
}
int getmin(int r) {
int ret = INF;
for (; r >= 0; r = (r & (r + 1)) - 1)
ret = min(ret, bit[r]);
return ret;
}
void update(int idx, int val) {
for (; idx < n; idx = idx | (idx + 1))
bit[idx] = min(bit[idx], val);
}
};Nota: es posible implementar un Árbol de Fenwick que pueda manejar consultas arbitrarias de mínimo en un rango y actualizaciones arbitrarias. El artículo Efficient Range Minimum Queries using Binary Indexed Trees describe ese enfoque. Sin embargo, con ese enfoque hay que mantener un segundo árbol indexado binario sobre los datos, con una estructura ligeramente distinta, ya que un solo árbol no alcanza para guardar los valores de todos los elementos del arreglo. La implementación también es bastante más difícil comparada con la implementación normal para sumas.
Suma en un arreglo bidimensional
Como se afirmó antes, es muy fácil implementar un Árbol de Fenwick para un arreglo multidimensional.
struct FenwickTree2D {
vector<vector<int>> bit;
int n, m;
// init(...) { ... }
int sum(int x, int y) {
int ret = 0;
for (int i = x; i >= 0; i = (i & (i + 1)) - 1)
for (int j = y; j >= 0; j = (j & (j + 1)) - 1)
ret += bit[i][j];
return ret;
}
void add(int x, int y, int delta) {
for (int i = x; i < n; i = i | (i + 1))
for (int j = y; j < m; j = j | (j + 1))
bit[i][j] += delta;
}
};Enfoque con indexación desde uno
Para este enfoque cambiamos un poco los requisitos y la definición de y . Queremos que guarde la suma de . Esto cambia un poco la implementación, y permite una definición igualmente agradable de :
def sum(int r):
res = 0
while (r > 0):
res += t[r]
r = g(r)
return res
def increase(int i, int delta):
for all j with g(j) < i <= j:
t[j] += deltaEl cálculo de se define como: invertir el último bit activado en la representación binaria de .
El último bit activado se puede extraer usando , así que la operación se puede expresar como:
Y no es difícil ver que hay que cambiar todos los valores en la secuencia cuando se quiere actualizar , donde se define como:
Como se puede ver, la principal ventaja de este enfoque es que las operaciones binarias se complementan de forma muy elegante.
La siguiente implementación se puede usar como las otras implementaciones, pero internamente usa indexación desde uno.
struct FenwickTreeOneBasedIndexing {
vector<int> bit; // Árbol de Fenwick (BIT)
int n;
FenwickTreeOneBasedIndexing(int n) {
this->n = n + 1;
bit.assign(n + 1, 0);
}
FenwickTreeOneBasedIndexing(vector<int> a)
: FenwickTreeOneBasedIndexing(a.size()) {
for (size_t i = 0; i < a.size(); i++)
add(i, a[i]);
}
int sum(int idx) {
int ret = 0;
for (++idx; idx > 0; idx -= idx & -idx)
ret += bit[idx];
return ret;
}
int sum(int l, int r) {
return sum(r) - sum(l - 1);
}
void add(int idx, int delta) {
for (++idx; idx < n; idx += idx & -idx)
bit[idx] += delta;
}
};Operaciones de rango
Un Árbol de Fenwick puede soportar las siguientes operaciones de rango:
- Actualización puntual y consulta de rango
- Actualización de rango y consulta puntual
- Actualización de rango y consulta de rango
1. Actualización puntual y consulta de rango
Esto es simplemente el Árbol de Fenwick ordinario explicado arriba.
2. Actualización de rango y consulta puntual
Con trucos simples también podemos hacer las operaciones inversas: incrementar rangos y consultar valores individuales.
Sea el Árbol de Fenwick inicializado con ceros.
Supongamos que queremos incrementar el intervalo en .
Hacemos dos operaciones de actualización puntual sobre el Árbol de Fenwick: add(l, x) y add(r+1, -x).
Si queremos obtener el valor de , solo necesitamos tomar la suma de prefijos usando el método ordinario de suma de rango. Para ver por qué esto es cierto, podemos enfocarnos de nuevo en la operación de incremento anterior. Si , entonces las dos operaciones de actualización no tienen efecto sobre la consulta y obtenemos la suma . Si , entonces obtenemos la respuesta por la primera operación de actualización. Y si , entonces la segunda operación de actualización cancelará el efecto de la primera.
La siguiente implementación usa indexación desde uno.
void add(int idx, int val) {
for (++idx; idx < n; idx += idx & -idx)
bit[idx] += val;
}
void range_add(int l, int r, int val) {
add(l, val);
add(r + 1, -val);
}
int point_query(int idx) {
int ret = 0;
for (++idx; idx > 0; idx -= idx & -idx)
ret += bit[idx];
return ret;
}Nota: por supuesto también es posible incrementar un solo punto con range_add(i, i, val).
3. Actualización de rango y consulta de rango
Para soportar tanto actualizaciones de rango como consultas de rango usaremos dos BIT, a saber y , inicializados con ceros.
Supongamos que queremos incrementar el intervalo en el valor .
De forma similar al método anterior, hacemos dos actualizaciones puntuales sobre : add(B1, l, x) y add(B1, r+1, -x).
Y también actualizamos . Los detalles se explican más adelante.
def range_add(l, r, x):
add(B1, l, x)
add(B1, r+1, -x)
add(B2, l, x*(l-1))
add(B2, r+1, -x*r))Después de la actualización de rango la consulta de suma de rango debería devolver los siguientes valores:
Podemos escribir la suma de rango como diferencia de dos términos, donde usamos para el primer término y para el segundo. La diferencia de las consultas nos dará la suma de prefijos sobre .
La última expresión es exactamente igual a los términos requeridos. Así podemos usar para eliminar los términos extra cuando multiplicamos .
Podemos encontrar sumas de rangos arbitrarios calculando las sumas de prefijos para y y tomando de nuevo su diferencia.
def add(b, idx, x):
while idx <= N:
b[idx] += x
idx += idx & -idx
def range_add(l,r,x):
add(B1, l, x)
add(B1, r+1, -x)
add(B2, l, x*(l-1))
add(B2, r+1, -x*r)
def sum(b, idx):
total = 0
while idx > 0:
total += b[idx]
idx -= idx & -idx
return total
def prefix_sum(idx):
return sum(B1, idx)*idx - sum(B2, idx)
def range_sum(l, r):
return prefix_sum(r) - prefix_sum(l-1)Problemas de práctica
- UVA 12086 - Potentiometers
- LOJ 1112 - Curious Robin Hood
- LOJ 1266 - Points in Rectangle
- Codechef - SPREAD
- SPOJ - CTRICK
- SPOJ - MATSUM
- SPOJ - DQUERY
- SPOJ - NKTEAM
- SPOJ - YODANESS
- SRM 310 - FloatingMedian
- SPOJ - Ada and Behives
- Hackerearth - Counting in Byteland
- DevSkill - Shan and String (archived)
- Codeforces - Little Artem and Time Machine
- Codeforces - Hanoi Factory
- SPOJ - Tulip and Numbers
- SPOJ - SUMSUM
- SPOJ - Sabir and Gifts
- SPOJ - The Permutation Game Again
- SPOJ - Zig when you Zag
- SPOJ - Cryon
- SPOJ - Weird Points
- SPOJ - Its a Murder
- SPOJ - Bored of Suffixes and Prefixes
- SPOJ - Mega Inversions
- Codeforces - Subsequences
- Codeforces - Ball
- GYM - The Kamphaeng Phet’s Chedis
- Codeforces - Garlands
- Codeforces - Inversions after Shuffle
- GYM - Cairo Market
- Codeforces - Goodbye Souvenir
- SPOJ - Ada and Species
- Codeforces - Thor
- CSES - Forest Queries II
- Latin American Regionals 2017 - Fundraising