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 / pointsUpdatesOnlineStructureCost per op
dense, noneyes2D prefix sums
densepointyes2D Fenwick (Fenwick)
denserectangle add + rectangle sumyesfour 2D Fenwicks
sparse, large coordinatesnonenosweep + Fenwick
sparsenoneyespersistent segment tree / wavelet matrix
sparsepointnoCDQ (CDQ) or offline Fenwick of Fenwicks
sparsepointyesFenwick of sorted vectors with inner Fenwicks, or KD-tree /
non-rectangular region (half-plane, circle)anyyesKD-tree (KD-Tree)
dimensionsanynonested 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”.

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 set per 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;
}

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 events

Perimeter: 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.

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_set when 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 long 2D Fenwick is 32 MB.