Has Not Appeared
 0/6

Wavelet Tree

Authors: Benjamin Qi, Omri Jerbi

Wavelet trees support efficient queries for the kth minimum element in a range

Wavelet Tree

Wavelet trees are data structures that support efficient queries for the k-th minimum element in a range by maintaining a segment tree over values instead of indices.

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

Resources
IOI

Introduces Wavelet Tree

CF

Link in blog post is broken, check my comment.

Wavelet Tree Structure

To answer value-based queries efficiently, we'll create a segment tree where each node represents a range of values, instead of indices. Just like a normal segment tree, each subsequent level splits the range into two halves. Note that an index can appear in at most log(M)\log(M) nodes.

Wavelet Tree Visualization

Let's say our array is: [3,5,3,1,2,2,3,4,5,5][3,5,3,1,2,2,3,4,5,5] Each node has an array representing the indices of every number between ll and rr

Wavelet Tree Visualization

Solution - Range K-th Smallest

Before we solve this problem, let's consider a simpler version where we are asked, given a range, to count the number of occurrences of value xx.

Given a range ll, rr, count the number of occurrences of value x.

To calculate the number of occurrences from 𝑙𝑙 to 𝑟𝑟, we can use the following formula:

occurrences(l,r)=occurrences(r)occurrences(l)\begin{aligned} \texttt{occurrences}(l, r) = \texttt{occurrences}(r) - \texttt{occurrences}(l) \end{aligned}

This reduces the problem to counting the number of occurrences in a prefix.

One way to solve the problem is to go to the leaf node and perform a binary search for the number of indices less than 𝑟𝑟 However, let's explore a different approach that can also be extended to the second type of query.

Instead of binary searching on the leaf, we update 𝑟𝑟 as we recurse down the tree. If we can determine the position (index) of rr in the left and right children of a node, we can recurse down the tree and determine its position in the leaf node.

To find the position of 𝑟𝑟 in a node's left and right children, we need to determine how many indices are smaller than the middle value (mid) and precede 𝑟𝑟. This can be done using a prefix sum.

Let's define:

  • c[i]c[i] = as 11 if index\texttt{index}[ii] is smaller than mid otherwise 00
  • prefix_b\texttt{prefix\_b}[ii] as prefix sum of c[i]c[i]

Formally

c[i]={1,if index[i]<mid0,otherwisec[i] = \begin{cases} 1, & \texttt{if } \texttt{index}[i] < \texttt{mid} \\ 0, & \texttt{otherwise} \end{cases}
prefix_b[i]=prefix_b[i1]+c[i]\texttt{prefix\_b}[i] = \texttt{prefix\_b}[i - 1] + c[i]

To update rr as we recurse down, we do the following:

  • To know the value of rr if we recurse left, we use prefix_b\texttt{prefix\_b}[rr]
  • If we recurse right, we use rr - prefix_b\texttt{prefix\_b}[rr]

Now let's try to solve our main problem.

Given a range ll, rr find the k-th smallest element

We will determine whether the answer for a given node is in the left or the right segment. We can calculate how many times the elements within the segments' ranges appear in our range (l,r)(l, r) using our first type of query. Note that this also works for non-leaf nodes using the following formula:

occurrences(l,r)=rl\texttt{occurrences}(l, r) = r - l

Similar

This is similar to counting how many times a value appears up to index rr in our previous query. We did this by using the new rr value at the leaf node. But now, we consider the difference between the updated rr and ll

Therefore, the occurrences of the left node are

left_occurrences=prefix_b[r]prefix_b[l]\texttt{left\_occurrences} = \texttt{prefix\_b}[r] - \texttt{prefix\_b}[l]

Note that left_occurrences\texttt{left\_occurrences} is the number of indices between ll and rr whose value is less than mid\texttt{mid}

  • If left_occurrences\texttt{left\_occurrences} is greater or equal to kk, it means the kk-th smallest element is in the left subtree. Therefore, we update our range and recurse into the left child
  • If left_occurrences\texttt{left\_occurrences} is less than kk, it means the kk-th smallest element is in the right subtree. We adjust k by subtracting left_occurrences\texttt{left\_occurrences} from kk, update our range, and recurse into the right child

Notice

Notice we still update l,rl, r accordingly when we go left or right

The answer then will be the value of the node we end up on (leaf).

In conclusion we maintain our ranges l and r as we recurse down to our child, and when we reach the child node we can return rr - ll.

Implementation

Time Complexity: O(Qlog(M))\mathcal{O}(Q \cdot \log(M))

C++

#include <bits/stdc++.h>
using namespace std;
constexpr int MAX_VAL = 1e9 + 2;
struct Segment {
Segment *left = nullptr, *right = nullptr;
int l, r, mid;
bool children = false;
vector<pair<int, int>> indices; // index, value

Supporting updates

Let's support updates that change the value at index ii to yy.

We can traverse down to the leaf to remove the old element and also traverse down to add the new element.

Let's first solve for adding a new element; erasing is similar but the opposite.

So what do the updates change?

  • Our indices vector
  • Our prefix vector

At each step of our recursion, the indices vector will need to be modified; we need to insert the new index. Since we can no longer maintain a sorted vector of the indices, we will switch to a set.

On the other hand, to change the prefix vector, since each update could change our prefix vector a lot, we can't maintain just the normal vector. What we could do is use a sparse segment tree.

  • erasing and inserting can be done by just setting the value to 00 or 11 at the specific index
  • querying for a prefix can be done by querying the segment tree from 00 to ii

This approach is not memory efficient and requires a segment tree's implementation. A more friendly approach would be using an order statistics tree, which is a binary search tree implementation in C++ that allows efficient queries for the rank of elements in a set. Querying for a prefix would then be equivalent to order_of_key\texttt{order\_of\_key}(ii).

Implemention

Time Complexity: O(Qlog(M)log(N))\mathcal{O}(Q \cdot \log(M) \cdot \log(N))

C++

#include <bits/stdc++.h>
using namespace std;
#include <ext/pb_ds/assoc_container.hpp>
using namespace __gnu_pbds;
template <class T>
using Tree =
tree<T, null_type, less<T>, rb_tree_tag, tree_order_statistics_node_update>;

Problems

StatusSourceProblem NameDifficultyTags
CFNormal
Show TagsWavelet
SPOJNormal
Show TagsWavelet
COCINormal
Show TagsWavelet, Persistent Segtree
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!