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

MethodBuildQueryNotes
naive rescanfine for a handful of queries
precompute every range memory, only for
sparse tablethe default (Sparse Table)
segment treesupports updates
linear RMQ (64-bit masks + sparse table on blocks)fast and light for up to
Farach-Colton-Bendertheoretical; large constant
offline DSU sweep (Arpa’s trick)all queries known
sqrt treeany 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)]; }
};

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 methodPreprocessQueryNote
naive climbingfine for tiny trees
binary liftingthe contest default — short, flexible, supports path queries
Euler tour + sparse table~15 lines, excellent constant — what to actually write when matters
Tarjan offline (DSU)amortizedneeds every query known in advance
Farach-Colton-Benderasymptotically optimal, large constant
Schieber–Vishkinalso 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.

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

VariantApproach
range min with point updatessegment tree
range min with range add / assignlazy segment tree
min over tree pathsbinary 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 rangesegment tree storing (min, count)
2D range min2D 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.