Skip to Content

Árbol Wavelet

Introducción

Supongamos que tenemos un arreglo estático de enteros a0,a1,,aN1a_0, a_1, \dots, a_{N-1} que cumple 0ai<σ0\le a_i<\sigma, y queremos responder consultas online de la siguiente forma:

  • Hallar el kk-ésimo elemento más pequeño en el subarreglo contiguo a[l:r)a[l:r) (donde kk está indexado desde 0).
HechoFuenteNombreDificultadTagsSolución
YSRange K-th SmallestNormalWaveleten el módulo

En este módulo, introduciremos el concepto de un Árbol Wavelet (Wavelet Tree) para responder estas consultas de forma eficiente tanto en tiempo como en memoria. Cada solución del módulo se construye sobre la anterior.

SoluciónComplejidad temporal de consultaComplejidad espacial
Solución 1 del móduloO(logσlogN)O(\log \sigma\log N)O(Nlogσ)O(N\log \sigma)
Soluciones 2a / 2b del móduloO(logσ)O(\log \sigma)O(Nlogσ)O(N\log \sigma)
Solución 3 del móduloO(logσ)O(\log \sigma)O(N)O(N)
Árbol de Segmentos PersistenteO(logσ)O(\log \sigma)O(Nlogσ)O(N\log \sigma)
Opcional

Si σ>N\sigma>N, entonces podemos reducir σ\sigma a NN aplicando primero compresión de coordenadas a aa. Sin embargo, omitimos este paso en las soluciones de abajo, ya que logσ\log \sigma no es mucho mayor que logN\log N para las restricciones dadas.

Opcional

Los Árboles de Segmentos Persistentes pueden responder consultas en la misma complejidad temporal que los Árboles Wavelet. Sin embargo, los Árboles Wavelet usarán menos memoria.

Recursos

Leer estos recursos es opcional, a menos que las explicaciones del módulo resulten demasiado sucintas.

Recursos
FuenteRecursoNotas
IOIWavelet Trees for Competitive Programming

Introduce el Árbol Wavelet

CFIntro to New DS: Wavelet Trees
Opcional

El primer recurso también discute cómo soportar actualizaciones de aa incluyendo intercambios (swap(ai,ai+1)\text{swap}(a_i, a_{i+1})), entre otras.

K-ésimo más pequeño en un rango: Solución 1

Empecemos construyendo un Árbol de Segmentos sobre los valores [0,σ)[0, \sigma). Un nodo del árbol correspondiente a un rango de valores [vl,vr)[v_l, v_r) guardará

  1. Una lista que contiene los índices del arreglo aa con valores en ese rango, en orden creciente.
  2. Si el nodo no es una hoja (es decir, vl+1<vrv_l + 1 < v_r), punteros a sus dos nodos hijos, correspondientes a los rangos [vl,(vl+vr)/2)[v_l, (v_l+v_r)/2) y [(vl+vr)/2,vr)[(v_l+v_r)/2, v_r).

Un árbol donde cada nodo guarda una lista de todo lo que tiene debajo en orden se llama un Merge Sort Tree.

Para construir esta estructura de datos, empezamos en el nodo raíz correspondiente al rango [0,σ)[0,\sigma), partimos los índices [0,N)[0,N) entre sus dos hijos y construimos recursivamente cada hijo. Esto toma O(Nlogσ)O(N\log \sigma) en tiempo y memoria.

Para responder una consulta, de nuevo empezamos en el nodo raíz y luego caminamos recursivamente por el árbol hasta llegar al nodo hoja correspondiente al valor respuesta. Para determinar si caminar hacia el hijo izquierdo o el derecho del nodo actual, primero consultamos el número de índices en el vector de índices del hijo izquierdo en el rango [l,r)[l,r) y lo guardamos en una variable \texttt{num\\_left}.

  • Si k<\texttt{num\\_left}, entonces la respuesta es el kk-ésimo valor más pequeño en el hijo izquierdo.
  • En caso contrario, la respuesta es el (k-\texttt{num\\_left})-ésimo valor más pequeño en el hijo derecho.

Consultar el conteo en un solo nodo toma O(logN)O(\log N) usando búsqueda binaria, y el árbol tiene profundidad O(logσ)O(\log \sigma), así que en total una consulta toma O(logσlogN)O(\log \sigma\log N).

Implementación

Nota: fijamos σ=230\sigma=2^{30} para que cada nodo tenga longitud igual a una potencia de dos.

#include <bits/stdc++.h> using namespace std; int count_prefix(const vector<int> &v, int r) { return lower_bound(begin(v), end(v), r) - begin(v); } struct Wavelet { vector<int> inds; Wavelet *l, *r; void build(const vector<int> &A, int b) { if (b == 0 || inds.empty()) return; l = new Wavelet(); r = new Wavelet(); for (int x : inds) { if (A[x] & (1 << (b - 1))) r->inds.push_back(x); else l->inds.push_back(x); } l->build(A, b - 1); r->build(A, b - 1); } // k-th (0-indexed) smallest value, only considering A[l, r) int range_kth_smallest(int l, int r, int k, int b) { if (b == 0) return 0; int num_left = count_prefix(this->l->inds, r) - count_prefix(this->l->inds, l); if (k < num_left) return this->l->range_kth_smallest(l, r, k, b - 1); return (1 << (b - 1)) + this->r->range_kth_smallest(l, r, k - num_left, b - 1); } }; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int N, Q; cin >> N >> Q; vector<int> A(N); for (int &a : A) cin >> a; // build tree Wavelet *root = new Wavelet(); root->inds.resize(N); iota(begin(root->inds), end(root->inds), 0); const int MAX_BIT = 30; root->build(A, MAX_BIT); // answer queries for (int q = 0; q < Q; ++q) { int l, r, k; cin >> l >> r >> k; cout << root->range_kth_smallest(l, r, k, MAX_BIT) << "\n"; } }

K-ésimo más pequeño en un rango: Solución 2a

Nuestro objetivo en esta sección es quitar el factor logN\log N de la complejidad temporal de consulta de la solución 1 sin cambiar la complejidad espacial. Seguiremos guardando un vector de enteros en cada nodo del Árbol de Segmentos. Su longitud será la misma que antes, pero representará algo distinto.

Consideremos primero qué vector deberíamos guardar en el nodo raíz para calcular \texttt{num\\_left} sin búsqueda binaria. Lo más simple que podríamos hacer es guardar los valores de \texttt{count\\_prefix}(\texttt{this->l->inds}, r) para cada rr posible de 00 a NN inclusive. Es decir, todas las sumas de prefijos del bitvector de longitud NN con el ii-ésimo elemento igual a 11 si aia_i mapea al nodo hijo izquierdo, y 00 en caso contrario. Entonces \texttt{num\\_left} se puede calcular en tiempo constante simplemente restando dos sumas de prefijos.

En general, en cada nodo que no es hoja del Árbol de Segmentos, podemos primero construir un vector de bits de longitud igual a la subsecuencia de AA asociada con ese nodo, con 11s para valores que mapean al hijo izquierdo y 00s para los demás, y luego guardar sus sumas de prefijos en ese nodo.

Para responder consultas, a diferencia de la solución 1, necesitaremos modificar ll y rr a medida que caminamos por el árbol. En lugar de representar los índices ll-ésimo a rr-ésimo de AA, ahora representarán los índices ll-ésimo a rr-ésimo de la subsecuencia de AA asociada con el nodo actual.

Implementación

Nota: la implementación evita guardar \texttt{count\\_prefix}(\texttt{this->l->inds}, 0) ya que siempre es cero.

#include <bits/stdc++.h> using namespace std; int count_prefix(const vector<int> &v, int r) { return r == 0 ? 0 : v.at(r - 1); } struct Wavelet { vector<int> num_lefts; Wavelet *l, *r; void build(const vector<int> &A, int b) { if (b == 0 || A.empty()) return; l = new Wavelet(); r = new Wavelet(); int num_left = 0; vector<int> A0, A1; for (int x : A) { if (x & (1 << (b - 1))) { A1.push_back(x); } else { ++num_left; A0.push_back(x); } num_lefts.push_back(num_left); } l->build(A0, b - 1); r->build(A1, b - 1); } int range_kth_smallest(int l, int r, int k, int b) { if (b == 0) return 0; int pr = count_prefix(num_lefts, r); int pl = count_prefix(num_lefts, l); int num_left = pr - pl; if (k < num_left) return this->l->range_kth_smallest(pl, pr, k, b - 1); return (1 << (b - 1)) + this->r->range_kth_smallest(l - pl, r - pr, k - num_left, b - 1); } }; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int N, Q; cin >> N >> Q; vector<int> A(N); for (int &a : A) cin >> a; // build tree Wavelet *root = new Wavelet(); const int MAX_BIT = 30; root->build(A, MAX_BIT); // answer queries for (int q = 0; q < Q; ++q) { int l, r, k; cin >> l >> r >> k; cout << root->range_kth_smallest(l, r, k, MAX_BIT) << "\n"; } }

Solución 2b

La siguiente solución tiene la misma complejidad temporal y espacial que la anterior, pero el factor constante es mucho mejor. Específicamente, es más del doble de rápida y usa menos de una décima parte de la memoria.

Para lograr esto, concatena los vectores de bits de cada nivel en un solo vector de bits de longitud NN antes de tomar sumas de prefijos. Esta construcción se conoce como la Matriz Wavelet (Wavelet Matrix).

Se puede ver que el proceso de consulta es equivalente al de la solución de arriba (salvo traducir ll y rr por una constante).

#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int N, Q; cin >> N >> Q; vector<int> A(N); for (int &a : A) cin >> a; const int MAX_BIT = 30; vector<vector<int>> num_lefts(MAX_BIT); for (int b = MAX_BIT - 1; b >= 0; --b) { vector<int> A0, A1; int num_left = 0; for (int x : A) { if (x & (1 << b)) { A1.push_back(x); } else { ++num_left; A0.push_back(x); } num_lefts.at(b).push_back(num_left); } swap(A, A0); A.insert(end(A), begin(A1), end(A1)); } auto get_num_left = [&](int b, int r) { return r == 0 ? 0 : num_lefts.at(b).at(r - 1); }; auto range_kth_smallest = [&](int l, int r, int k) { int ans = 0; for (int b = MAX_BIT - 1; b >= 0; --b) { int pr = get_num_left(b, r); int pl = get_num_left(b, l); int num_left = pr - pl; if (k < num_left) { l = pl, r = pr; } else { k -= num_left; l = l - pl + num_lefts.at(b).back(); r = r - pr + num_lefts.at(b).back(); ans ^= 1 << b; } } return ans; }; for (int q = 0; q < Q; ++q) { int l, r, k; cin >> l >> r >> k; cout << range_kth_smallest(l, r, k) << "\n"; } }

K-ésimo más pequeño en un rango: Solución 3

Aquí discutimos cómo quitar el factor de logσ\log \sigma de la complejidad espacial.

Opcional

Toda esta sección se puede considerar opcional ya que el uso de memoria de la solución 2b ya está bien por debajo del límite.

El cuello de botella de memoria en la solución 2 es guardar las sumas de prefijos de logσ\log \sigma vectores de bits de longitud NN, lo que toma O(Nlogσ)O(N\log \sigma) enteros con el enfoque más directo. Sin embargo, si podemos reducir esto a O(Nlogσ)O(N\log \sigma) bits de información, podemos empaquetar estos bits en O(Nlogσ/W)O(N\log \sigma / W) palabras donde WW es el tamaño de palabra  (W=64W=64 en una arquitectura de 64 bits). Esto son O(N)O(N) palabras asumiendo 2W>σ2^W>\sigma (es decir, todos los enteros con los que trabajamos caben en una sola palabra).

Resta describir cómo guardar un solo vector de bits de longitud NN en O(N)O(N) bits permitiendo todavía consultas de suma de prefijos en tiempo constante. Específicamente, podemos guardar el vector de bits original en NN bits y solo las sumas de prefijos con longitud divisible por WW, tomando O(NlogN/W)O(N\log N/W) bits, que son O(N)O(N) bits asumiendo 2W>N2^W>N. Para responder una consulta de la rr-ésima suma de prefijos en tiempo constante, empezamos con la r/WW\lfloor r/W\rfloor\cdot W-ésima suma de prefijos y luego usamos operaciones built-in que corren en tiempo constante para sumar la contribución de los r%Wr\%W bits restantes (como \texttt{\\_\\_builtin\\_popcountll} para contar el número de bits activados en una palabra de 64 bits).

Implementación

#include <bits/stdc++.h> using namespace std; struct PrefixSummer { const int BITS = 64; // word size vector<uint64_t> packed; vector<int> psums; void init(const vector<bool> &v) { packed.resize(size(v) / BITS + 1); for (int i = 0; i < size(v); ++i) { if (v.at(i)) packed.at(i / BITS) |= 1ULL << (i % BITS); } psums = {0}; for (auto b : packed) psums.push_back(psums.back() + __builtin_popcountll(b)); } int count_prefix(int r) { return psums.at(r / BITS) + __builtin_popcountll(packed.at(r / BITS) & ((1ULL << (r % BITS)) - 1)); } int count() { return psums.back(); } }; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int N, Q; cin >> N >> Q; vector<int> A(N); for (int &a : A) cin >> a; const int MAX_BIT = 30; vector<PrefixSummer> num_lefts(MAX_BIT); for (int b = MAX_BIT - 1; b >= 0; --b) { vector<int> A0, A1; vector<bool> bitvec; for (int x : A) { if (x & (1 << b)) { bitvec.push_back(0); A1.push_back(x); } else { bitvec.push_back(1); A0.push_back(x); } } num_lefts.at(b).init(bitvec); swap(A, A0); A.insert(end(A), begin(A1), end(A1)); } auto range_kth_smallest = [&](int l, int r, int k) { int ans = 0; for (int b = MAX_BIT - 1; b >= 0; --b) { int pr = num_lefts.at(b).count_prefix(r); int pl = num_lefts.at(b).count_prefix(l); int num_left = pr - pl; if (k < num_left) { l = pl, r = pr; } else { k -= num_left; l = l - pl + num_lefts.at(b).count(); r = r - pr + num_lefts.at(b).count(); ans ^= 1 << b; } } return ans; }; for (int q = 0; q < Q; ++q) { int l, r, k; cin >> l >> r >> k; cout << range_kth_smallest(l, r, k) << "\n"; } }

Problemas

HechoFuenteNombreDificultadTagsSolución
SPOJK-queryNormalWavelet
COCI2021 - IndexNormalWavelet, Persistent SegtreeSolución
ACSmaller SumNormalWavelet, Persistent SegtreeSolución
YSRectangle SumNormalWavelet, Persistent SegtreeSolución
KattisEasy QueryMuy difícilWaveletSolución
GlobeX CupNinjaclasher's Wrath 2Muy difícilWavelet