Most “2D” problems (points in rectangles, range queries with a value condition, distinct values in a range) become 1D after sorting by one coordinate and sweeping with a Fenwick tree over the other. That’s time and memory, better than any 2D structure on both counts. Use a genuinely 2D structure only when the queries are forced online.
Choosing a structure
| Grid / points | Updates | Online | Structure | Cost per op |
|---|---|---|---|---|
| dense, | none | yes | 2D prefix sums | |
| dense | point | yes | 2D Fenwick (Fenwick) | |
| dense | rectangle add + rectangle sum | yes | four 2D Fenwicks | |
| sparse, large coordinates | none | no | sweep + Fenwick | |
| sparse | none | yes | persistent segment tree / wavelet matrix | |
| sparse | point | no | CDQ (CDQ) or offline Fenwick of Fenwicks | |
| sparse | point | yes | Fenwick of sorted vectors with inner Fenwicks, or KD-tree | / |
| non-rectangular region (half-plane, circle) | any | yes | KD-tree (KD-Tree) | |
| dimensions | any | no | nested CDQ |
Points in rectangles (offline sweep)
Write the rectangle as “prefix ” minus “prefix ”. Sort points and these events by ; insert points into a Fenwick over compressed when the sweep passes them.
vector<ll> rectSum(vector<array<ll, 3>> pts, const vector<array<ll, 4>>& qs) { // pts {x, y, w}, qs {x1, x2, y1, y2}
vector<ll> ys; for (auto& p : pts) ys.push_back(p[1]);
sort(all(ys)); ys.erase(unique(all(ys)), ys.end());
auto yi = [&](ll y) { return int(lower_bound(all(ys), y) - ys.begin()); };
sort(all(pts));
vector<tuple<ll, int, int>> ev; // (x, query, sign): uses points with px < x
for (int i = 0; i < sz(qs); i++) ev.emplace_back(qs[i][0], i, -1), ev.emplace_back(qs[i][1], i, 1);
sort(all(ev));
Fenwick<ll> fw(sz(ys)); vector<ll> res(sz(qs)); int p = 0;
for (auto [x, i, s] : ev) {
for (; p < sz(pts) && pts[p][0] < x; p++) fw.add(yi(pts[p][1]), pts[p][2]);
res[i] += s * fw.sum(yi(qs[i][2]), yi(qs[i][3]));
}
return res;
}With half-open rectangles, “points with ” is the whole tie-break rule. With closed rectangles, decide explicitly whether points at the same are processed before the query; most off-by-ones live there.
Dominance counting (“for each point, how many points are it in both coordinates”) is the same sweep. With a third coordinate it’s the 3D partial order problem, solved by CDQ in .
Static rectangle add
Read at points: a rectangle add is four signed corners of a 2D difference array; the value at is the sum of corners with , , which is a dominance sum, so the same sweep applies.
Read over rectangles: keep four Fenwicks over of corner weights , , , (with sign), and evaluate
over the corners with , , where is the sum of all cells . A query rectangle is four signed prefix values. Values reach , so use __int128 or a modular type. Library Checker exercises this exact family: “Static Rectangle Add Rectangle Sum”, “Rectangle Sum”, “Rectangle Add Point Get”.
Proof: the four-Fenwick formula
The cell value is (2D difference array). Summing over the cells with and , a corner with and contributes for each of the cells it dominates, and other corners contribute nothing. Expanding separates the query coordinates from the corner coordinates, so each of the four sums is a dominance sum over corners, maintained by one Fenwick each during the sweep.
Distinct values in a range
Let be the previous index with the same value ( if none). The number of distinct values in equals the number of with , a 2D dominance count.
- Offline: sweep ; when enters, add at and at ; the answer is the Fenwick sum over .
- Online: a persistent segment tree, version = the array above after processing (PST).
- With point updates: a point update changes of positions (found with a
setper value), so it’s dominance counting with insertions and deletions: CDQ over time, .
vector<int> distinct(const vector<int>& a, const vector<pair<int, int>>& qs) { // [l, r)
int n = sz(a); vector<vector<int>> at(n + 1); vector<int> res(sz(qs));
for (int i = 0; i < sz(qs); i++) at[qs[i].second].push_back(i);
map<int, int> last; Fenwick<int> fw(n);
for (int r = 0; r <= n; r++) {
for (int i : at[r]) res[i] = fw.sum(qs[i].first, r);
if (r == n) break;
if (auto it = last.find(a[r]); it != last.end()) fw.add(it->second, -1);
fw.add(r, 1); last[a[r]] = r;
}
return res;
}Proof: the sweep counts exactly the distinct values
After processing positions , the Fenwick holds a 1 exactly at the last occurrence (before ) of each value. A value occurs in iff its last occurrence before is . So the sum over counts each value present in the range exactly once.
The same “keep only the last occurrence” sweep gives the sum of distinct values, “is there a duplicate”, the minimum gap between equal values, and range mex (Offline Techniques).
Area of a union of rectangles
Sweep ; a segment tree over the elementary -intervals keeps cnt[v] (how many active rectangles cover node ‘s whole range as a canonical node) and len[v] (covered length inside ‘s range). No lazy propagation is needed.
struct Cover {
int n; vector<ll> ys, len; vector<int> cnt; // elementary intervals [ys[i], ys[i+1])
Cover(vector<ll> y) : ys(y), n(sz(y) - 1), len(4 * sz(y)), cnt(4 * sz(y)) {}
void upd(int v, int l, int r, int ql, int qr, int d) {
if (qr <= l || r <= ql) return;
if (ql <= l && r <= qr) cnt[v] += d;
else { int m = (l + r) / 2; upd(2 * v, l, m, ql, qr, d); upd(2 * v + 1, m, r, ql, qr, d); }
len[v] = cnt[v] ? ys[r] - ys[l] : r - l == 1 ? 0 : len[2 * v] + len[2 * v + 1];
}
};
// events (x, y1, y2, +1/-1) sorted by x; area += len[1] * (x_next - x) between consecutive eventsPerimeter: add at every event for the vertical edges, and do the same sweep in for horizontal ones (or track the number of covered segments per node). “Area covered by at least rectangles”: store len for coverage per node.
Proof:
len[1]is the covered lengthInvariant: equals the length of the part of ‘s range covered by the intervals currently assigned (as canonical nodes) to or its descendants. If , an interval covers all of ‘s range, so the whole range counts. Otherwise the covered part is the union of the children’s covered parts. Each update recomputes
lenon exactly the nodes whose subtree changed (the canonical nodes and their ancestors). Every removal hits the same canonical nodes as the matching insertion, socntnever goes negative and nothing has to be pushed down: intervals assigned to an ancestor are accounted for by the ancestor’scnt. At the root, the invariant is the covered length of the union.
Online 2D with point updates
- Fenwick of sorted vectors: for each Fenwick node over , collect (offline) all values that will ever be inserted into it, sort, and give it an inner Fenwick. Updates and queries are with memory. This needs the set of points in advance but answers queries online.
- Fenwick of
ordered_setwhen even that is not known, with a larger constant. - Sparse 2D segment tree (segment tree over whose nodes are dynamic segment trees over ): time and memory, often too much.
- KD-tree with periodic rebuilds: per query, memory.
Rules of thumb
- If a query has two conditions ( and ), sweep one and index the other.
- Three conditions: sweep one, CDQ on the second, Fenwick on the third.
- 2D difference arrays: inclusion-exclusion with corners; beyond find a reformulation that removes a dimension.
- Memory: a
long long2D Fenwick is 32 MB.