Queries like “-th smallest in ”, “how many values in lie in ”, predecessor/successor within a range. Static and online: wavelet matrix or persistent segment tree, . Static and offline: sweep with a Fenwick tree. With point updates: Fenwick of dynamic segment trees, or offline parallel binary search.

Methods

MethodQueryMemoryUpdatesOnline
sort the range (naive)yesyes
wavelet matrix bitsnoyes
persistent segment tree over values intsnoyes (PST)
merge sort treecount , -th noyes
offline sweep by value + Fenwickcount yesno
overall binary search (parallel) + Fenwick-th amortizedyesno (PBS)
Fenwick of dynamic value trees (“tree in tree”)yesyes
sqrt decomposition with sorted blocks per countyesyes
Mo’s algorithm + a value-indexed Fenwick per countnono (offline)

Wavelet matrix

Compress values to . At level (from the top bit down), record bit of every value in the current order, then stably move zeros before ones for the next level. A range at level maps to among zeros and to among ones, where is the number of zeros on that level.

struct BitVec {
    vector<ull> b; vector<int> pre;
    BitVec(int n = 0) : b(n / 64 + 1), pre(n / 64 + 1) {}
    void set(int i) { b[i >> 6] |= 1ULL << (i & 63); }
    void build() { for (int j = 1; j < sz(b); j++) pre[j] = pre[j - 1] + __builtin_popcountll(b[j - 1]); }
    int rank1(int i) const { return pre[i >> 6] + __builtin_popcountll(b[i >> 6] & ((1ULL << (i & 63)) - 1)); }
    int rank0(int i) const { return i - rank1(i); }
};
template<class T> struct WaveletMatrix {
    int n, lg; vector<T> vals; vector<BitVec> lv; vector<int> Z;
    WaveletMatrix(const vector<T>& a) : n(sz(a)), vals(a) {
        sort(all(vals)); vals.erase(unique(all(vals)), vals.end());
        lg = __lg(max(sz(vals), 1)) + 1;
        vector<int> cur(n), nxt(n);
        for (int i = 0; i < n; i++) cur[i] = lower_bound(all(vals), a[i]) - vals.begin();
        lv.assign(lg, BitVec(n)); Z.assign(lg, 0);
        for (int h = lg - 1; h >= 0; h--) {
            int z = 0;
            for (int i = 0; i < n; i++) if (cur[i] >> h & 1) lv[h].set(i); else nxt[z++] = cur[i];
            lv[h].build(); Z[h] = z;
            for (int i = 0; i < n; i++) if (cur[i] >> h & 1) nxt[z++] = cur[i];
            swap(cur, nxt);
        }
    }
    T kth(int l, int r, int k) const {                 // 0-indexed k-th smallest of [l, r)
        int res = 0;
        for (int h = lg - 1; h >= 0; h--) {
            int l0 = lv[h].rank0(l), r0 = lv[h].rank0(r);
            if (k < r0 - l0) l = l0, r = r0;
            else k -= r0 - l0, res |= 1 << h, l += Z[h] - l0, r += Z[h] - r0;
        }
        return vals[res];
    }
    int countLess(int l, int r, T x) const {           // # i in [l, r) with a[i] < x
        int u = lower_bound(all(vals), x) - vals.begin(), res = 0;
        for (int h = lg - 1; h >= 0; h--) {
            int l0 = lv[h].rank0(l), r0 = lv[h].rank0(r);
            if (u >> h & 1) res += r0 - l0, l += Z[h] - l0, r += Z[h] - r0;
            else l = l0, r = r0;
        }
        return res;
    }
};

Predecessor of in the range: c = countLess(l, r, x), then kth(l, r, c - 1) if . Successor: kth(l, r, c) if . Storing prefix sums of original values per level also gives “sum of the smallest”.

No update support: the bit vectors would need re-partitioning on every change, which defeats the structure’s whole point. Use an offline sweep or the Fenwick-of-value-trees variant below instead when updates are needed. Beyond contest range queries, this is the structure behind succinct text indexing: the FM-index (a compressed full-text index built on the Burrows-Wheeler transform, used by DNA aligners like BWA and Bowtie), compressed suffix arrays, and document retrieval (“which documents contain this pattern”) all use a wavelet tree/matrix for their rank queries, precisely because the bits-not-integers memory saving matters at that scale.

Merge sort tree

A segment tree whose node stores the sorted values of its range (built by merging children). “Count in ” binary searches in canonical nodes: . Fractional cascading (store, for each element of a node, its position in each child’s list) removes the inner binary searches for queries, at the cost of significant bookkeeping for a modest constant — rarely worth writing in a contest, but the standard example of the technique. It’s simple and hard to get wrong, but the wavelet matrix dominates it on time and memory.

Updates: replacing an element needs re-sorting every node on its root path, worst case — the merge sort tree is a static structure by design. If updates are genuinely needed, use the offline BIT sweep above, a sqrt decomposition with sorted blocks (rebuild one block in ), or replace each node’s sorted vector with a multiset/BIT ( updates, worse constant, node overhead).

With updates: Fenwick of value trees

A Fenwick tree over positions whose nodes are dynamic segment trees over values. A point update touches Fenwick nodes, each . A query collects the roots for prefix (sign ) and prefix (sign ) and descends all of them at once.

int kth(int l, int r, int k) {                         // positions [l, r), 0-indexed k
    vector<int> P, M;
    for (int i = r; i > 0; i &= i - 1) P.push_back(root[i - 1]);
    for (int i = l; i > 0; i &= i - 1) M.push_back(root[i - 1]);
    int lo = 0, hi = V;
    while (hi - lo > 1) {
        int mid = (lo + hi) / 2, left = 0;
        for (int p : P) left += cnt[lc[p]];
        for (int p : M) left -= cnt[lc[p]];
        if (k < left) { for (int& p : P) p = lc[p]; for (int& p : M) p = lc[p]; hi = mid; }
        else { k -= left; for (int& p : P) p = rc[p]; for (int& p : M) p = rc[p]; lo = mid; }
    }
    return lo;
}

per operation and memory. The offline alternative (overall binary search with a single Fenwick) uses memory and is usually preferable.

On trees

-th smallest on the path –: persistent value trees along root paths, then descend the combination . In subtrees: Euler tour order turns it into the array problem.

QueryHow
median of -th with
number of distinct values in a rangenot an order statistic: offline sweep with last occurrences, or persistent tree on positions (below)
-th distinct value in a rangethe distinct-count structure below, plus a descent
sum of over a rangecount and sum of values below (wavelet with sums, or PST with sums)
range modesqrt decomposition (Sqrt Decomposition)
-th smallest of the whole arraynth_element, or a Fenwick descent if dynamic

Distinct values in a range

CSES “Distinct Values Queries”: count distinct values in . Not an order statistic (no comparison structure needed), but the same toolbox solves it.

Key idea: a value is “new” at position iff is its last occurrence so far looking backward, i.e. prev[i] (the previous equal element, or ) is . So the count over is the number of with prev[i] < l.

vector<int> lastOccurrence(const vector<int>& a) {     // prev[i] = largest j < i with a[j] == a[i], or -1
    int n = sz(a); vector<int> order(n), prev(n, -1);
    iota(all(order), 0);
    stable_sort(all(order), [&](int i, int j) { return a[i] < a[j]; });
    for (int k = 1; k < n; k++) if (a[order[k - 1]] == a[order[k]]) prev[order[k]] = order[k - 1];
    return prev;
}
// Offline: sweep r, keep a 1 at i iff i is currently the last occurrence of its value among [0, r).
vector<int> distinctInRanges(const vector<int>& a, const vector<pair<int, int>>& qs) {   // [l, r)
    int n = sz(a), q = sz(qs);
    vector<int> prev = lastOccurrence(a), res(q);
    vector<vector<int>> byRight(n + 1);
    for (int i = 0; i < q; i++) byRight[qs[i].second].push_back(i);
    Fenwick<int> fw(n);
    for (int r = 0; r <= n; r++) {
        for (int i : byRight[r]) res[i] = fw.sum(qs[i].first, r);
        if (r == n) break;
        if (prev[r] >= 0) fw.add(prev[r], -1);
        fw.add(r, 1);
    }
    return res;
}

offline. For online queries, build a persistent segment tree over positions: version is the indicator array “is the last occurrence of its value among ”, and query(l, r) descends version summing over … equivalently sums the indicator over directly.

struct DistinctOnline {                                 // query(l, r): # distinct values in [l, r)
    int n; vector<int> lc{0}, rc{0}, cnt{0}, roots{0};
    DistinctOnline(const vector<int>& a) : n(sz(a)) {
        vector<int> prev = lastOccurrence(a);
        for (int r = 0; r < n; r++) {
            int root = roots.back();
            if (prev[r] >= 0) root = add(root, 0, n, prev[r], -1);  // the old last-occurrence stops counting
            roots.push_back(add(root, 0, n, r, 1));                 // r becomes the new last occurrence
        }
    }
    int add(int p, int l, int r, int pos, int v) {
        int q = sz(lc); lc.push_back(lc[p]); rc.push_back(rc[p]); cnt.push_back(cnt[p] + v);
        if (r - l > 1) {
            int m = (l + r) / 2;
            if (pos < m) lc[q] = add(lc[p], l, m, pos, v); else rc[q] = add(rc[p], m, r, pos, v);
        }
        return q;
    }
    int sumFrom(int p, int l, int r, int x) const {      // sum of the indicator over [x, n), in version p
        if (!p || r <= x) return 0;
        if (x <= l) return cnt[p];
        int m = (l + r) / 2;
        return sumFrom(lc[p], l, m, x) + sumFrom(rc[p], m, r, x);
    }
    int query(int l, int r) const { return l < r ? sumFrom(roots[r], 0, n, l) : 0; }  // version r, positions >= l
};

per query, build and memory.