Has Not Appeared
 0/7

Wavelet Tree

Author: Benjamin Qi

Contributor: Omri Jerbi

Efficiently querying for the kth minimum element in a subarray.

Introduction

Suppose we have a static array of integers a0,a1,,aN1a_0, a_1, \dots, a_{N-1} satisfying 0ai<σ0\le a_i<\sigma, and we want to answer online queries of the following form:

  • Find the kk-th smallest element in the contiguous subarray a[l:r)a[l:r) (where kk is 0-indexed).

Focus Problem – try your best to solve this problem before continuing!

In this module, we will introduce the concept of a Wavelet Tree to answer these queries efficiently with respect to both time and memory. Each module solution will build on the previous one.

SolutionQuery Time ComplexityMemory Complexity
Module Solution 1O(logσlogN)O(\log \sigma\log N)O(Nlogσ)O(N\log \sigma)
Module Solutions 2a / 2bO(logσ)O(\log \sigma)O(Nlogσ)O(N\log \sigma)
Module Solution 3O(logσ)O(\log \sigma)O(N)O(N)
Persistent Segment TreeO(logσ)O(\log \sigma)O(Nlogσ)O(N\log \sigma)

Optional

If σ>N\sigma>N, then we can reduce σ\sigma to NN by first applying coordinate compression to aa. However, we omit this step in the solutions below, since logσ\log \sigma isn't much larger than logN\log N for the given constraints.

Optional

Persistent segment trees can answer queries in the same time complexity as Wavelet Trees. However, Wavelet Trees will use less memory.

Resources

Reading these resources is optional, unless you find the in-module explanations too succinct.

Optional

The first resource also discusses how to support updates to aa including swapping (swap(ai,ai+1)\text{swap}(a_i, a_{i+1})), among others.

Range Kth Smallest: Solution 1

Let's start by building a segment tree on the values [0,σ)[0, \sigma). A segment tree node corresponding to a range of values [vl,vr)[v_l, v_r) will store

  1. A list containing the indices of the array aa with values in that range, in increasing order.
  2. If the node is not a leaf (that is, vl+1<vrv_l + 1 < v_r), pointers to its two child nodes, corresponding to the ranges [vl,(vl+vr)/2)[v_l, (v_l+v_r)/2) and [(vl+vr)/2,vr)[(v_l+v_r)/2, v_r).

A tree where each node stores a list of everything under it in sorted order is called a merge-sort tree.

To build this data structure, we start at the root node corresponding to the range [0,σ)[0,\sigma), partition the indices [0,N)[0,N) among its two children, and recursively build each child. This takes O(Nlogσ)O(N\log \sigma) time and memory.

To answer a query, we again start at the root node, then recursively walk down the tree until we reach the leaf node corresponding to the answer value. To determine whether to walk down into the left child or the right child of the current node, we first query the number of indices in the index vector of the left child in the range [l,r)[l,r) and store it into a variable num_left\texttt{num\_left}.

  • If k<num_leftk<\texttt{num\_left}, then the answer is the kkth-smallest value in the left child.
  • Otherwise, the answer is the (knum_left)(k-\texttt{num\_left})-th smallest value in the right child.

Querying the count in a single node takes O(logN)O(\log N) time using binary search, and the tree has depth O(logσ)O(\log \sigma), so in a total a query takes O(logσlogN)O(\log \sigma\log N) time.

Implementation

Note: we set σ=230\sigma=2^{30} so that every node has length equal to a power of two.

C++

#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;

Range Kth Smallest: Solution 2a

Our goal in this section is to remove the logN\log N factor from the query time complexity of solution 1 without changing the memory complexity. We'll continue to store a vector of integers at each segment tree node. Its length will be the same as before, but it will represent something different.

Let's first consider what vector we should store at the root node to compute num_left\texttt{num\_left} without binary search. The simplest thing we could do is to store the values of count_prefix(this->l->inds,r)\texttt{count\_prefix}(\texttt{this->l->inds}, r) for each possible rr from 00 to NN inclusive. That is, all prefix sums of the length-NN bitvector with iith element equal to 11 if aia_i maps to the left child node, and 00 otherwise. Then num_left\texttt{num\_left} can be computed in constant time just by subtracting two prefix sums.

In general, at each non-leaf node of the segment tree, we can first construct a bit vector of length equal to the subsequence of AA associated with that node with 11s for values that map to the left child node and 00s for others, and then store its prefix sums at that node.

To answer queries, unlike solution 1, we'll need to modify ll and rr as we walk down the tree. Instead of representing the llth through rrth indices of AA, they'll now represent the llth through rrth indices of the subsequence of AA associated with the current node.

Implementation

Note: The implementation avoids storing count_prefix(this->l->inds,0)\texttt{count\_prefix}(\texttt{this->l->inds}, 0) since it's always zero.

C++

#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;

Solution 2b

The following solution has the same time and memory complexity as the previous one, but the constant factor is much better. Specifically, it's more than twice as fast, and uses less than one tenth the memory!

To accomplish this, it concatenates the bitvectors at each level into a single bitvector of length NN before taking prefix sums. This construction is known as the Wavelet Matrix.

The query process can be seen to be equivalent to that of the solution above (up to translating ll and rr by a constant).

C++

#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);

Range Kth Smallest: Solution 3

Here we discuss how to remove the factor of logσ\log \sigma from the memory complexity.

Optional

This entire section can be considered optional since the memory usage of solution 2b is already well below the limit.

The memory bottleneck in solution 2 is storing the prefix sums of logσ\log \sigma length-NN bitvectors, which takes O(Nlogσ)O(N\log \sigma) integers using the most straightforward approach. However, if we can reduce this to O(Nlogσ)O(N\log \sigma) bits of information, we can pack these bits into O(Nlogσ/W)O(N\log \sigma / W) words where WW is the word size (W=64W=64 on a 64-bit architecture). This is O(N)O(N) words assuming 2W>σ2^W>\sigma (that is, all the integers we're working with fit into a single word).

It remains to describe how to store a single length-NN bitvector in O(N)O(N) bits while still allowing constant time prefix sum queries. Specifically, we can store the original bitvector in NN bits and only the sums of prefixes with length divisible by WW, taking O(NlogN/W)O(N\log N/W) bits, which is O(N)O(N) bits assuming 2W>N2^W>N. To answer a query for the rrth prefix sum in constant time, we start with the r/WW\lfloor r/W\rfloor\cdot Wth prefix sum and then use built-in operations that run in constant time to add the contribution of the remaining r%Wr\%W bits (like __builtin_popcountll\texttt{\_\_builtin\_popcountll} to count the number of bits set in a 64-bit word).

Implementation

C++

#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) {

Problems

Suggestion

Try to solve all the problems below with Wavelet Tree. Other data structures will also pass under the given time and memory limits, although they will often add a log factor to the time or memory complexities.

StatusSourceProblem NameDifficultyTags
SPOJNormal
Show TagsWavelet
COCINormal
Show TagsPersistent Segtree, Wavelet
ACNormal
Show TagsPersistent Segtree, Wavelet
YSNormal
Show TagsPersistent Segtree, Wavelet
KattisVery Hard
Show TagsWavelet
GlobeX CupVery Hard
Show TagsWavelet

Module Progress:

Join the USACO Forum!

Stuck on a problem, or don't understand a module? Join the USACO Forum and get help from other competitive programmers!