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

SituationTechnique
queries on ranges with a value thresholdsweep by value with a BIT over positions
queries answerable by maintaining something as growssweep , structure indexed by (“last occurrence” trick)
only deletions (DSU, sets that can’t delete)reverse time: deletions become insertions
additions and deletions, structure supports undosegment tree over time + rollback
a window of updates (oldest removed, newest added), structure supports undoqueue undo trick
the answer for each query is a monotone event timeparallel binary search
incremental add/remove of single elements is cheapMo’s algorithm
update/query interactions with 2-3 static conditionsCDQ
only insertions, static structure with decomposable queriesbinary grouping (static-to-dynamic)
many queries, few distinct “shapes”group queries by key, answer each group once
LCA queries on a static treeTarjan’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.

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:

  • 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”.

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.

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.

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.
  • 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).

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.