RMQ on a static array is equivalent to LCA on a tree: an RMQ is an LCA in the Cartesian tree, and an LCA is a RMQ on the Euler tour depths. Both admit preprocessing and queries. In practice the sparse table is the default, the 64-word “linear RMQ” is the fast choice, and offline queries can use a DSU sweep.
Methods
| Method | Build | Query | Notes |
|---|---|---|---|
| naive rescan | fine for a handful of queries | ||
| precompute every range | memory, only for | ||
| sparse table | the default (Sparse Table) | ||
| segment tree | supports updates | ||
| linear RMQ (64-bit masks + sparse table on blocks) | fast and light for up to | ||
| Farach-Colton-Bender | theoretical; large constant | ||
| offline DSU sweep (Arpa’s trick) | all queries known | ||
| sqrt tree | any associative op, with point updates (Sqrt) |
Linear RMQ with bitmask stacks
For each , mask[i] encodes the monotonic stack of the last 64 positions: bit means position is a suffix minimum of the window ending at . A short range ending at is answered by the highest surviving bit within its length. Longer ranges take two such answers plus a sparse table over blocks of 64.
template<class T, class Cmp = less<T>> struct LinearRMQ {
int n; vector<T> v; vector<ull> mask; vector<int> sp; Cmp cmp;
int better(int x, int y) const { return cmp(v[x], v[y]) ? x : y; }
static int msb(ull x) { return 63 - __builtin_clzll(x); }
int small(int r, int len = 64) const { return r - msb(len == 64 ? mask[r] : mask[r] & ((1ULL << len) - 1)); }
LinearRMQ(const vector<T>& a, Cmp c = Cmp()) : n(sz(a)), v(a), mask(n), cmp(c) {
ull at = 0;
for (int i = 0; i < n; i++) {
at <<= 1;
while (at && better(i, i - msb(at & -at)) == i) at ^= at & -at;
mask[i] = at |= 1;
}
int b = n / 64; if (!b) return;
int L = __lg(b) + 1; sp.assign(b * L, 0);
for (int i = 0; i < b; i++) sp[i] = small(64 * i + 63);
for (int j = 1; j < L; j++) for (int i = 0; i + (1 << j) <= b; i++)
sp[b * j + i] = better(sp[b * (j - 1) + i], sp[b * (j - 1) + i + (1 << (j - 1))]);
}
int argmin(int l, int r) const { // [l, r)
r--;
if (r - l + 1 <= 64) return small(r, r - l + 1);
int ans = better(small(l + 63), small(r)), x = l / 64 + 1, y = r / 64 - 1;
if (x <= y) { int j = __lg(y - x + 1), b = n / 64; ans = better(ans, better(sp[b * j + x], sp[b * j + y - (1 << j) + 1])); }
return ans;
}
T query(int l, int r) const { return v[argmin(l, r)]; }
};Proof: the bitmask stack and linear RMQ are correct
Invariant on
mask[i]: bit ofmask[i]is set iff is a suffix minimum of the window , i.e. isbetter-or-equal to every element of — the same monotonic-stack invariant proved for the sliding-window deque in Two Pointers. Induction on : shifting left ages every bit by one position (offset , still correctly describing suffix minima of the old window relative to the new right end, since no element to the left of changed), then thewhileloop removes exactly the closest surviving positions that are no longer suffix minima once is included (those with worse than — if survived meant everything in , and now either , in which case stops being a suffix minimum once joins, or , in which case remains valid and the loop correctly stops there since positions are processed closest-first — the lowest surviving bit is the one closest to ), and finally setting bit 0 records that itself is trivially a suffix minimum of .small(r,len)returns , i.e. the farthest surviving suffix-minimum position within the last entries; by the invariant, surviving positions have values that are non-increasing as you move farther from (each survived because it beat everything closer), so the farthest survivor within the window is the minimum of the whole window .argminfor callssmalldirectly; for longer ranges it takessmallof the partial first/last blocks of 64 plus a sparse table over complete 64-blocks (correct by the sparse-table proof above, since whole-block minima are idempotent underbetter), and takes the best of all three — covering exactly once each piece, so the overall min is correct.Tested against
min_elementon 2000 random arrays (size up to 300) and 20 random ranges each — all match.
Proof: Arpa's offline DSU sweep is correct
Key fact at the moment pops (because , and was on the monotonic stack, meaning ): then , since . Setting
p[j] = rtherefore records that, from this point on, and are interchangeable range-starts: for any , , because ‘s minimum is already , so prepending to changes nothing. So each union link is a sound “this start point can be redirected to that one without changing the answer for any later right endpoint” step. Induction on the find chain:find(l)follows these links until reaching a position withp[l^*] = l^*, i.e. is still on the stack at time — meaning by the stack invariant, so . By the key fact applied transitively along the chain , , so is exactly .Tested against
min_elementon 2000 random arrays (size up to 50) with random query batches — all match.
RMQ and LCA
- LCA RMQ: in the Euler tour depth array consecutive values differ by exactly 1, so a block of length has one of shapes. Precompute every in-block answer for every shape, a sparse table over blocks (cost for tour entries), and a query splits into a prefix of one block, some whole blocks, and a suffix of another — three lookups. That’s Farach-Colton-Bender: /, large constant, rarely worth the code. The “precompute every shape” trick is called four Russians (Arlazarov, Dinic, Kronrod, Faradzhev — originally for boolean matrix multiplication), and the same idea reappears in bitset DP and precomputed popcount tables.
- RMQ LCA: build the Cartesian tree in with a monotonic stack. is the value at . So general RMQ is / too.
- In practice for LCA: Euler tour + sparse table (fast, ), or binary lifting when you also need -th ancestors or path aggregates (LCA).
| LCA method | Preprocess | Query | Note |
|---|---|---|---|
| naive climbing | fine for tiny trees | ||
| binary lifting | the contest default — short, flexible, supports path queries | ||
| Euler tour + sparse table | ~15 lines, excellent constant — what to actually write when matters | ||
| Tarjan offline (DSU) | amortized | needs every query known in advance | |
| Farach-Colton-Bender | asymptotically optimal, large constant | ||
| Schieber–Vishkin | also optimal, bit-trick based, smaller constant than FCB in practice |
with a tiny constant beats with a huge one for every a contest will throw at you — reach for binary lifting by default, Euler tour + sparse table when queries matter, and FCB/Schieber–Vishkin only when profiling says so.
Proof: both reductions are correct
LCA RMQ. Let
first[v]be the index of ‘s first occurrence in the Euler tour, and let be the tour’s depth array. Every vertex on the path from to (and from up to it) appears in the tour betweenfirst[u]andfirst[v], each at depth (every ancestor of or visited in that window is itself an ancestor of, or equal to, , hence at depth it), and itself is visited at least once in that window at exactly its own depth. So , achieved at an occurrence of , giving itself via the argmin. Consecutive tour steps move to a parent or a child, changing depth by exactly , which is the special structure Farach-Colton-Bender exploits for .
RMQ LCA. Build the Cartesian tree (heap-ordered by value, BST-ordered by index — correctness of this construction is proved in Monotonic Stack). For indices , every index in is a descendant of in the Cartesian tree (BST order: the subtree rooted at any node spans a contiguous index range, and ‘s common subtree-range must contain ), and the heap property makes ‘s value every value in its subtree, in particular every for . Since ‘s own index lies in (it’s between its descendants and in BST order) and its value is a lower bound for the whole range, it must be the range minimum.
Offline RMQ with a DSU (Arpa’s trick)
Sweep . Keep the monotonic stack of suffix minima of . When pops index , attach to in a DSU. Then find(l) is the position of the minimum of .
vector<int> offlineArgmin(const vector<ll>& a, const vector<pair<int, int>>& qs) { // [l, r)
int n = sz(a); vector<vector<int>> at(n);
for (int i = 0; i < sz(qs); i++) at[qs[i].second - 1].push_back(i);
vector<int> p(n), st, res(sz(qs)); iota(all(p), 0);
auto find = [&](int x) { while (p[x] != x) x = p[x] = p[p[x]]; return x; };
for (int r = 0; r < n; r++) {
while (!st.empty() && a[r] < a[st.back()]) p[st.back()] = r, st.pop_back();
st.push_back(r);
for (int i : at[r]) res[i] = find(qs[i].first);
}
return res;
}Variants
| Variant | Approach |
|---|---|
| range min with point updates | segment tree |
| range min with range add / assign | lazy segment tree |
| min over tree paths | binary lifting, HLD, or Kruskal reconstruction tree (for bottleneck queries) |
| min over every window of length | monotonic deque, |
| position of the first element to the right of | segment tree descent, or binary lifting on the sparse table |
| number of distinct minima / count of minimum in range | segment tree storing (min, count) |
| 2D range min | 2D sparse table ( memory, see Sparse Table) or per-row 1D tables |
RMQ also shows up as a subroutine rather than the main problem: “for each subarray, the minimum splits it” divide-and-conquer (the same recursion that proves the largest-rectangle formula and builds the Cartesian tree), and range queries inside D&C DP.