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 satisfying , and we want to answer online queries of the following form:
- Find the -th smallest element in the contiguous subarray (where 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.
Solution | Query Time Complexity | Memory Complexity |
---|---|---|
Module Solution 1 | ||
Module Solutions 2a / 2b | ||
Module Solution 3 | ||
Persistent Segment Tree |
Optional
If , then we can reduce to by first applying coordinate compression to . However, we omit this step in the solutions below, since isn't much larger than 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.
Resources | |||||
---|---|---|---|---|---|
IOI | Introduces Wavelet Tree | ||||
CF |
Optional
The first resource also discusses how to support updates to including swapping (), among others.
Range Kth Smallest: Solution 1
Let's start by building a segment tree on the values . A segment tree node corresponding to a range of values will store
- A list containing the indices of the array with values in that range, in increasing order.
- If the node is not a leaf (that is, ), pointers to its two child nodes, corresponding to the ranges and .
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 , partition the indices among its two children, and recursively build each child. This takes 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 and store it into a variable .
- If , then the answer is the th-smallest value in the left child.
- Otherwise, the answer is the -th smallest value in the right child.
Querying the count in a single node takes time using binary search, and the tree has depth , so in a total a query takes time.
Implementation
Note: we set 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 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 without binary search. The simplest thing we could do is to store the values of for each possible from to inclusive. That is, all prefix sums of the length- bitvector with th element equal to if maps to the left child node, and otherwise. Then 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 associated with that node with s for values that map to the left child node and s for others, and then store its prefix sums at that node.
To answer queries, unlike solution 1, we'll need to modify and as we walk down the tree. Instead of representing the th through th indices of , they'll now represent the th through th indices of the subsequence of associated with the current node.
Implementation
Note: The implementation avoids storing 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 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 and 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 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 length- bitvectors, which takes integers using the most straightforward approach. However, if we can reduce this to bits of information, we can pack these bits into words where is the word size ( on a 64-bit architecture). This is words assuming (that is, all the integers we're working with fit into a single word).
It remains to describe how to store a single length- bitvector in bits while still allowing constant time prefix sum queries. Specifically, we can store the original bitvector in bits and only the sums of prefixes with length divisible by , taking bits, which is bits assuming . To answer a query for the th prefix sum in constant time, we start with the th prefix sum and then use built-in operations that run in constant time to add the contribution of the remaining bits (like 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 sizevector<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.
Status | Source | Problem Name | Difficulty | Tags | ||
---|---|---|---|---|---|---|
SPOJ | Normal | Show TagsWavelet | ||||
COCI | Normal | Show TagsPersistent Segtree, Wavelet | ||||
AC | Normal | Show TagsPersistent Segtree, Wavelet | ||||
YS | Normal | Show TagsPersistent Segtree, Wavelet | ||||
Kattis | Very Hard | Show TagsWavelet | ||||
GlobeX Cup | Very 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!