Offline range queries where the window can be extended and shrunk by one element in (or ): count distinct, pairs of equal values, sum of , mex with a value-sqrt, mode with a count-of-counts. Reorder the queries so that the total pointer movement is .
Classic Mo
template<class Add, class Rem, class Ans>
void mo(int n, const vector<pair<int, int>>& qs, Add add, Rem rem, Ans ans) { // queries [l, r)
int q = sz(qs), B = max(1, int(n / sqrt(max(q, 1))));
vector<int> ord(q); iota(all(ord), 0);
sort(all(ord), [&](int i, int j) {
int bi = qs[i].first / B, bj = qs[j].first / B;
if (bi != bj) return bi < bj;
return bi & 1 ? qs[i].second > qs[j].second : qs[i].second < qs[j].second;
});
int L = 0, R = 0;
for (int id : ord) {
auto [l, r] = qs[id];
while (L > l) add(--L); // grow first, then shrink:
while (R < r) add(R++); // the window is never "negative"
while (L < l) rem(L++);
while (R > r) rem(--R);
ans(id);
}
}The alternating direction of (“odd-even sort”) saves about a third of the movement. Hilbert order (sort queries by the index of on a Hilbert curve of side ) is another 20-40% in practice:
ll hilbert(int x, int y, int k = 20) { // 2^k >= n
ll d = 0;
for (int s = 1 << (k - 1); s; s >>= 1) {
int rx = (x & s) > 0, ry = (y & s) > 0;
d += ll(s) * s * ((3 * rx) ^ ry);
if (!ry) { if (rx) x = s - 1 - x, y = s - 1 - y; swap(x, y); }
}
return d;
}Proof: pointer moves with
Queries are grouped by the block of , so there are at most groups. Within a group, is monotone, so moves at most per group, plus at most when switching groups: in total. Each query moves by at most within its group (both endpoints lie in the same block), and switching groups moves forward by at most more, so moves in total. The sum is minimised at , giving . Correctness does not depend on the order: every answer is read when the window equals exactly , and
add/remmaintain the window’s state as an invariant. Doing all extensions before shrinks ensures that throughout, soremis only called on elements that are in the window.
When the add/remove cost is not : if add is (Fenwick for “count of values ”), the total is . Usually better: keep updates and queries via a value-sqrt structure (counts per value + counts per value block). Then the total is . Classic targets: Codeforces 940F “Machine Learning” (mex of frequency counts, with updates), and “count of distinct values in a range” under point updates.
Mo with updates
Queries become where = number of updates before the query. Sort by with . Moving applies/undoes one update: if the position is inside the window, remove the old value and add the new one; always write the array.
struct Upd { int pos, before, after; }; // fill "before" by simulating the array while reading
auto apply = [&](int k, bool fwd) {
int p = ups[k].pos, v = fwd ? ups[k].after : ups[k].before;
if (L <= p && p < R) { rem(p); a[p] = v; add(p); } else a[p] = v;
};
// per query: while (T < q.t) apply(T++, 1); while (T > q.t) apply(--T, 0); then move L and R as usualProof: total moves when
Group queries by the pair of blocks : there are groups. Within a group, is monotone, so moves per group: in total. and move per query inside a group and per group change in the worst case (but only sweeps once per -block, so ). Total: . With and every term is . Using instead makes the time term , quadratic.
Mo on trees (path queries)
Euler tour where each vertex appears twice: at entry () and exit (), array length . For a path with and :
- if , the range is ;
- otherwise the range is and must be added separately (toggle it before reading the answer and toggle it back).
Both add and rem become toggle(vertex): a vertex whose two occurrences both lie in the range cancels out.
Proof: the vertices appearing exactly once in the range are exactly the path (minus possibly the LCA)
Case : the range runs from entering to entering . A vertex appears once iff we entered it and haven’t left it by the time we enter , which means is an ancestor of that is entered after (or at) , i.e. a vertex on the path from down to . Every other vertex in the range was fully explored (entered and exited) inside it. Case : the range starts at the exit of . Vertices on the path from up to (but excluding) are exited inside the range and were entered before it: they appear once. Vertices on the path from down to (excluding ) are entered inside the range and not yet exited: once. itself was entered before and is exited after: zero times, hence the separate toggle. Any other vertex is either entirely inside (twice) or entirely outside (zero).
Subtree queries need no trick: a subtree is the contiguous range of the usual single-occurrence Euler tour, and plain array Mo’s applies with no toggling. They’re usually better done by small-to-large (DSU on Tree) or a Fenwick tree over that same Euler-tour range; reach for Mo’s only when the aggregate resists both.
| Situation | Method | Cost |
|---|---|---|
| Aggregate is associative, no updates | binary lifting | |
| Aggregate is associative, with updates | HLD + segment tree | |
| Aggregate needs add/remove semantics | Mo’s on trees | |
| Counting over all paths | centroid decomposition | |
| Small vertex subsets per query | virtual tree |
Mo’s on trees is the fallback for aggregates like “number of distinct values” that HLD cannot merge — its niche is real but narrow. The Hilbert-curve speedup for the query order applies here too, same as array Mo’s.
Rollback Mo (add-only)
When removing is impossible but you can add and undo (max, mode with only increasing counts, DSU connectivity, linear basis, “max distance between equal values”): group by the block of , sort by . For each block, reset to empty with at the block end. For each query, grow (persistently within the block), snapshot, add on the left, answer, roll back. Queries entirely inside one block are brute-forced from empty.
template<class AL, class AR, class Snap, class Roll, class Reset, class Ans>
void moRollback(int n, const vector<pair<int, int>>& qs, AL addL, AR addR, Snap snap, Roll roll, Reset reset, Ans ans) {
int q = sz(qs); if (!q) return;
int B = max(1, int(n / sqrt(q)));
vector<int> ord(q); iota(all(ord), 0);
sort(all(ord), [&](int i, int j) { return pair(qs[i].first / B, qs[i].second) < pair(qs[j].first / B, qs[j].second); });
for (int i = 0; i < q;) {
int blk = qs[ord[i]].first / B, end = min(n, (blk + 1) * B), r = end;
reset();
for (; i < q && qs[ord[i]].first / B == blk; i++) {
auto [l, rr] = qs[ord[i]];
if (rr <= end) { snap(); for (int j = l; j < rr; j++) addR(j); }
else { while (r < rr) addR(r++); snap(); for (int j = end - 1; j >= l; j--) addL(j); }
ans(ord[i]); roll();
}
}
}Implement rollback with a history stack of (cell, old value) pairs pushed in add; snap records the stack size and the scalar answer. For range connectivity (components using edges , CodeChef GERALD07), add merges in a DSU with union by size and no path compression, and rollback pops merges: .
Proof: rollback Mo does adds and the answers are correct
Within one -block, only increases, so the right part grows by at most per block: . Each query adds at most elements on the left (from to the block end) and undoes them, and brute-forced queries cost at most each: . With the total is . Correctness: before the left additions, the state is exactly , built purely by additions; after adding it is exactly ; rollback restores because the undo log contains every changed cell.
Second offline Mo (Mo with prefix queries)
For answers of the form (range inversions, pairs with ) where moving a pointer by one is too expensive ( each makes ). Moving from to changes the answer by :
- the first term is a prefix quantity, precomputed for every in one sweep;
- the second term is “elements against the prefix ”, recorded as one task per pointer run (not per step) at prefix .
A final sweep inserts into a structure with insert and query, and answers each task by iterating its range. Total: queries + inserts, memory. The answers come out as differences, so take prefix sums along the processing order. Luogu P5047 (range inversions), P4887.
Proof: the decomposition is exact and the cost is
Since is summed over pairs, the change from adding on the right of is , which equals the sum over minus the sum over ; the left side is symmetric with over the window. Shrinking subtracts the same quantities. The number of task elements equals the total pointer movement of classic Mo, , and each costs one query; there are inserts of each. The structure holds only one prefix at a time, so memory is (the tasks are ranges, not items).
Other variants
| Variant | Idea | Cost |
|---|---|---|
| two ranges (e.g. two Euler-tour subtrees, CF 1767F) | four pointers, sort by blocks of then , block | |
| online Mo (forced online, with point updates) | store the structure for every pair of blocks; query = stored + loose elements; update touches structures | : adds per query, structures per update, copies in memory |
| bitset Mo | window state is a bitset of values; answer = bitset operations (e.g. “can / “) | |
| Mo on 2D (submatrices) | pointers per side | -ish, rarely good |
Checklist
- Block size , not , when .
- Extend before shrinking; with half-open windows, start from .
- Compress values; keep counts in plain arrays, not maps.
- If the answer needs “max of counts”, maintain count-of-counts (removal never needs a search because the max drops by at most one), or use rollback Mo.
- Prefer an offline sweep when the query is decomposable (distinct values in a range: sweep , keep only the last occurrence in a Fenwick).