If all queries are known in advance, you can reorder them, reverse time, batch them, or split time into a tree. A simpler structure then suffices. Forced-online problems (parameters XORed with the last answer) exist precisely to block these tricks.
Decision list
| Situation | Technique |
|---|---|
| queries on ranges with a value threshold | sweep by value with a BIT over positions |
| queries answerable by maintaining something as grows | sweep , structure indexed by (“last occurrence” trick) |
| only deletions (DSU, sets that can’t delete) | reverse time: deletions become insertions |
| additions and deletions, structure supports undo | segment tree over time + rollback |
| a window of updates (oldest removed, newest added), structure supports undo | queue undo trick |
| the answer for each query is a monotone event time | parallel binary search |
| incremental add/remove of single elements is cheap | Mo’s algorithm |
| update/query interactions with 2-3 static conditions | CDQ |
| only insertions, static structure with decomposable queries | binary grouping (static-to-dynamic) |
| many queries, few distinct “shapes” | group queries by key, answer each group once |
| LCA queries on a static tree | Tarjan’s offline LCA: DFS order + DSU, |
Sweeping
Sweep by value. “Count elements in ”: sort elements by value and queries by ; insert positions into a BIT as grows. , memory, which beats a merge sort tree on both counts.
Proof: the value-sweep answers each query correctly
Process elements and queries together in increasing order of value, breaking ties so every element with value is inserted before query is answered (e.g. insert all elements of value before answering queries with threshold ). At the moment query is answered, the BIT contains exactly the positions of elements with value inserted so far — which, by the tie-breaking rule, is exactly the set of elements with value in the whole array, since nothing with value has been inserted yet and everything with value has. The BIT range-sum over then counts exactly the elements in with value . Each element is inserted once () and each query answered once (), giving total.
Sweep by right end with last occurrences. “Number of distinct values in ”: sweep ; keep a 1 at the last occurrence of each value seen so far. The answer is the BIT sum over . The same scheme answers:
Proof: the last-occurrence BIT sum equals the distinct count
Sort queries by and sweep from to ; whenever a value is seen, clear its old mark (at its previous occurrence, if any) and set a new mark at position . Invariant after processing position : the BIT has exactly one marked position per distinct value appearing in , namely that value’s last (rightmost) occurrence so far. At the moment a query is answered (with the sweep exactly at ), the marked positions in are exactly the rightmost occurrences of the distinct values in — one mark per distinct value, by the invariant, and no value has two marks since each new occurrence clears the previous one. Restricting the BIT sum to keeps exactly the values whose mark (rightmost occurrence in ) falls in . A value appears in iff its rightmost occurrence within is : if it occurs anywhere in , its rightmost occurrence overall is at least that position, so ; conversely if its rightmost occurrence is and (by the sweep) , that occurrence itself lies in . So the marks landing in are exactly the distinct values of , one mark each.
- sum of distinct values in a range;
- “does the range have a duplicate” (the max over of previous-occurrence must be );
- minimum distance between equal values in a range (sweep , point-update at with , range min over );
- mex of a range (segment tree over values storing last occurrence, find the first value with last occurrence ).
Sweep with “latest/earliest event” per position handles “after which update does position first become ” style questions combined with parallel binary search.
Reversing time
Union-find can’t delete, but a deletion run backwards is a union. Apply all deletions first (the final state), then walk the operations backwards, re-adding and answering. Applies to: edge removals with connectivity or component-size queries, “remove cells from a grid and count islands”, deleting elements from a set where you need predecessor/successor via DSU “next pointer”.
Proof: reversed deletions behave as unions
Let be the sequence of states, where is obtained from by deleting one element (an edge, a grid cell, a set element). A query asked “between deletion and ” needs the structure in state . Walking backwards from (the final, fully-deleted state, built directly) to , going from to means adding back exactly the element deleted at step — which is a pure insertion/union, never a removal, since by definition of the forward process. So the reversed walk visits every state using only insertions, letting a union-only structure (DSU) answer every query by answering it when the reversed walk reaches the matching state, then reporting results back to the user in original time order.
Segment tree over time (offline deletion)
Each element exists during an interval of time . Insert it into the segment-tree nodes covering that interval. DFS the tree: on entering a node apply its elements, at a leaf answer the query, on leaving undo them. Requires only apply + undo last (rollback DSU, DSU with parity, XOR basis with a history stack, a knapsack DP layer stack).
Total cost: applications times the apply cost. Rollback DSU uses union by size without path compression, so each find is . Details: Dynamic Connectivity.
Proof: total applications
A segment tree over leaves decomposes any range into at most disjoint canonical nodes (the standard range-update decomposition: at each of the depths, at most two nodes of the decomposition touch that depth, one from the left boundary and one from the right, so the count is linear in depth, i.e. overall). Each of the up to elements with a bounded lifetime is inserted into nodes, so the total number of (element, node) insertions is when . The DFS applies each node’s stored elements exactly once on entry and undoes them exactly once on exit, so the total number of apply/undo calls equals the total number of insertions, .
Queue undo trick
Turns a structure with apply / undo the last apply (a stack) into one that can also remove the oldest update (a queue), at amortized applies per operation. Typical use: two pointers over edges where the window must stay bipartite or acyclic (CF 1386C “Joker”).
The stack holds two kinds of updates: A (scheduled to be popped soon, oldest first) and B (pushed normally). To pop the oldest: if no A remains, undo everything and re-push it all as A in reverse order. Otherwise undo from the top until as many A’s as B’s have been removed (or the B’s are exhausted), then re-push the B’s and then the A’s so the A’s are on top, and finally undo the oldest A. Each update is re-applied times amortized.
Proof: amortized re-applications per update
Give every update a rank: each time it is popped and re-pushed as part of a reshuffle (the “undo until as many A’s as B’s” step, or the full-flip step), its rank increases by exactly 1. The reshuffle step stops as soon as the number of popped B’s reaches the number of popped A’s (or the A-region is exhausted), so a reshuffle that increases some update’s rank from to always pairs it with at least as many freshly pushed (rank-0-since-last-reshuffle) updates as there are rank- updates being promoted. Charging each promotion to the newly-pushed updates it consumes, an update cannot be promoted to rank until at least as many new pushes have occurred as the number of rank- updates currently alive — the same “amount of fresh work needed to justify a promotion at least matches the batch being promoted” structure as a binary counter’s carry chain or small-to-large merging. Since the total number of live updates is at most , no update can reach rank higher than (each rank level needs the surviving batch to have been matched by at least its own size in fresh pushes, so the batch at rank has “cost” pushes behind it, bounded by ). Summing ranks over all updates — which equals the total number of re-applications — gives over operations, i.e. amortized per update.
template<class DS, class Upd> struct QueueUndo {
DS ds; vector<pair<Upd, bool>> st; int bottom = 0; // bool: is A
void push(const Upd& u, bool a = false) { ds.apply(u); st.push_back({u, a}); }
void pop() {
while (bottom < sz(st) && !st[bottom].second) bottom++;
if (bottom == sz(st)) { // no A left: flip everything to A
vector<Upd> all;
while (!st.empty()) all.push_back(st.back().first), st.pop_back(), ds.undo();
for (auto& u : all) push(u, true);
bottom = 0;
}
vector<Upd> t[2];
do { t[st.back().second].push_back(st.back().first); st.pop_back(); ds.undo(); }
while (t[1].size() < t[0].size() && sz(st) > bottom);
for (int i : {0, 1}) { reverse(all(t[i])); for (auto& u : t[i]) push(u, i); }
st.pop_back(); ds.undo(); // the oldest A
}
};ds.apply must record exactly one undo entry per call, even when nothing changes.
Static to dynamic (binary grouping)
For a static structure (build once, query many) whose queries are decomposable (the answer over a union is a combination of answers over the parts: counts, sums, min, existence), support insertions by keeping groups of sizes like the bits of a binary counter. Inserting merges all full low groups with the new item into the first empty slot and rebuilds it.
- Each item is rebuilt times: total for build cost .
- A query asks all groups.
Proof: binary grouping costs total and groups per query
This is exactly a binary counter: group sizes are powers of 2, and inserting an item is a to the counter, merging (rebuilding) every full low-order group it carries into, exactly as binary addition carries. An item is rebuilt once per carry it participates in; by the standard binary-counter amortized argument, a bit at position flips (carries) at most once per increments, so across insertions an item is swept into rebuilds over its lifetime, each rebuild of a size- group costing . Summing geometrically, total cost is (or when is linear). At any moment the number of non-empty groups equals the number of set bits in ‘s binary representation (the counter value), which is , so a decomposable query touches groups.
- Deletions: keep a second instance of deleted items and subtract (for counts), or rebuild periodically.
- Classic use: dynamic Aho-Corasick (insert patterns, query occurrence counts in texts), dynamic convex hull for queries, KD-tree with insertions.
Batching (sqrt on time)
Process operations in blocks of size . At the start of each block rebuild a static structure from everything before the block (cost ), and answer each query in the block from the structure plus a brute-force scan over the at most pending updates. The total is , minimised at . It’s the general escape hatch when updates break a fast static structure (Sqrt Decomposition).
Proof: total cost , minimised at
There are blocks, each rebuilt once at cost : rebuilding contributes . Within a block, each of its (at most ) queries scans at most pending updates, so each block’s query-answering costs , and there are blocks, contributing . Total: . Viewing this as a function of , its derivative w.r.t. is , zero at , and the second derivative is positive there, so minimises the sum (equivalently, by AM-GM, with equality exactly at ), giving total cost .
When you cannot go offline
Genuinely interactive problems, queries that depend on earlier answers (forced-online, blocking exactly these tricks), and streaming settings. Forced-online problems typically XOR the query parameters with the previous answer; when you see that, these techniques are ruled out by design and you need a persistent or truly dynamic structure instead.