Store the array as maximal runs of equal values in an ordered map. Range assign collapses many runs into one. Any range operation then visits the runs it covers. It’s also called ODT (“old driver tree”) after CF 896C.
Implementation
template<class T> struct IntervalMap {
map<ll, T> m; // run start -> value; key hi is a sentinel
IntervalMap(ll lo, ll hi, T init) { m[lo] = init; m[hi] = init; }
auto split(ll x) { // make x the start of a run
auto it = prev(m.upper_bound(x));
return it->first == x ? it : m.emplace_hint(next(it), x, it->second);
}
void assign(ll l, ll r, T v) { // a[l, r) = v
auto ir = split(r), il = split(l);
m.erase(next(il), ir); il->second = v;
}
template<class F> void each(ll l, ll r, F f) { // f(start, end, value&) for every run in [l, r)
auto ir = split(r), il = split(l);
for (auto it = il; it != ir; ++it) f(it->first, next(it)->first, it->second);
}
};Split before . With this map + emplace_hint version either order works, because splitting never erases a node. In the common set<Node> implementation, splitting re-inserts a run, so splitting first can invalidate the iterator for the run containing .
Complexity
Each assign removes all runs inside it and adds at most 3, so the number of runs created overall is , and assign is amortized. The cost of each is proportional to the number of runs visited.
Proof:
assignis amortized, and totaleach/assignwork across runs isUse the potential (current number of runs in the map).
split(l)andsplit(r)each add at most one new run (splitting one run into two), so oneassigncall increases by at most 2 from splitting, then deletes every run strictly between the split points and inserts one run for — a net change that is bounded (deleting runs and adding changes by ). So eachassignincreases by at most (from the two splits) while the map operations themselves (the twosplits, each, plus erasing runs, ) cost for the splits and for the erase. Since starts at , is increased by perassign, and every unit of erased was created by some earliersplit/assign, the total number of runs ever created over queries on an array that started with runs is — each creation charged to insert and to eventually erase, so total map-maintenance work is , i.e. amortized perassign. Any call toeach(or the erase loop insideassign) visits only runs that exist right now, and every run it visits either survives (bounded total existence) or gets deleted (charged once, as above) — so the total work across alleach/assigncalls visiting runs is also .
- With random operations where a constant fraction are assigns (CF 896C), the expected number of runs stays -ish and every operation is fast.
- Adversarial tests with few assigns make
eachper call. Then you need a real structure (lazy segment tree, segment tree beats).
Where it is the intended or best tool
- Range assign + anything weird on the runs: -th smallest in a range (collect runs, sort), sum of -th powers mod in a range (per run:
pow(value, x) * length). These are the CF 896C operations. - Union of intervals / painting:
IntervalMap<bool>; total painted length viaeach. - “Colour the range with , report how many cells change colour”: count lengths of runs whose value differs from before assigning. Amortized deterministically, because each assign removes the runs it counts.
Proof: the colour-change counting query is total, deterministically
This is exactly
eachimmediately followed byassignover the same range: the runs visited byeachto count mismatched lengths are precisely the runs thatassign’s erase loop is about to delete, so this operation never visits a run “for free” that it doesn’t also charge to a deletion. By the run-creation/deletion accounting above, every run deleted was created once (at a cost of ) and is now being both counted and deleted (another ), so this combined operation’s cost is exactly accounted for by the same -total-run-creations argument — no adversarial input can force more visits than runs ever created, unlike plaineachalone (which, as the note says, can be forced to repeatedly if assigns are rare, since nothing then deletes the runs being re-scanned).
- Maintaining a set of disjoint intervals (merge on insert, split on delete), like “free memory blocks”, “covered positions”, or “segments of equal values with counts”.
- Amortized arguments on trees: the same idea along HLD chains (assign a colour to a root path, count colour changes) gives the classic amortized “access” analysis used in link-cut tree style problems.
A set<pair<ll,ll>> of disjoint intervals with the same split/merge discipline is the minimal version when there’s no value to store.