Circle Union
Area covered by at least one of circles (Green’s theorem over the uncovered boundary arcs).
vector<pair<PD, double>> cs; // {center, radius}
double area = circleUnionArea(cs);Complexity: .
double circleUnionArea(vector<pair<PD, double>> cs) {
const double PI = acos(-1.0), EPS = 1e-12;
int n = cs.size();
double res = 0;
for (int i = 0; i < n; i++) {
auto [ci, ri] = cs[i];
if (ri <= EPS) continue;
vector<pair<double, double>> covered;
bool inside = false;
for (int j = 0; j < n && !inside; j++) {
if (j == i) continue;
auto [cj, rj] = cs[j];
double d = abs_(cj - ci);
if (d <= EPS && fabs(ri - rj) <= EPS) { // duplicate circle: keep the first copy only
if (j < i) inside = true;
continue;
}
if (rj >= ri + d - EPS) { inside = true; continue; } // circle i inside circle j
if (ri >= rj + d - EPS || ri + rj <= d + EPS) continue; // j inside i, or disjoint / touching
double mid = atan2(cj.y - ci.y, cj.x - ci.x);
double half = acos(max(-1.0, min(1.0, (ri * ri + d * d - rj * rj) / (2 * ri * d))));
double a = mid - half, b = mid + half; // covered arc of circle i
while (a < -PI) { a += 2 * PI; b += 2 * PI; }
while (a >= PI) { a -= 2 * PI; b -= 2 * PI; }
if (b > PI) { covered.push_back({a, PI}); covered.push_back({-PI, b - 2 * PI}); }
else covered.push_back({a, b});
}
if (inside) continue;
sort(covered.begin(), covered.end());
auto addArc = [&](double t1, double t2) {
if (t2 - t1 <= 0) return;
PD u1{cos(t1), sin(t1)}, u2{cos(t2), sin(t2)};
res += (ri * ri * (t2 - t1) + ri * cross(ci, u2 - u1)) / 2;
};
double cur = -PI;
for (auto [a, b] : covered) {
if (a > cur) addArc(cur, a);
cur = max(cur, b);
}
addArc(cur, PI);
}
return res;
}Idea: for each circle , find which arcs of its boundary are covered by some other circle (a chord-intersection half-angle computation, the same construction as circle-circle intersection), merge those covered intervals, and apply Green’s theorem only over the uncovered arcs — their union, over all circles, is exactly the boundary of the total union region. A circle entirely inside another contributes nothing; exact duplicates keep only the first copy.
Proof: Green's theorem over the uncovered arcs
Green’s theorem gives the (signed) area enclosed by a closed curve as . The boundary of the union (each a disk) consists exactly of the points on some circle ‘s boundary that are not strictly inside any other disk — every other point of each circle’s boundary is interior to the union, not on its topological boundary. So summing restricted to the uncovered arc of each circle, over all circles, gives exactly the line integral around the true boundary of the union (traversed with each circle’s own arcs in their natural CCW direction, which is consistent around the whole union boundary since each disk’s boundary is already CCW and the uncovered pieces splice together into one or more CCW closed curves bounding the union).
For a single arc of circle from angle to (parametrized , ): (the second cross product is , the constant area-sweep rate of a point going around the circle itself). Integrating: where (since directly, and
crossis linear in its second argument so it passes through the integral). Halving gives exactlyaddArc’s formula. Each circle’s covered-arc intervals are found and merged with a standard sorted-interval-union sweep (curtracks the rightmost end seen so far; a new interval starting aftercuropens an uncovered gap[cur, a], added viaaddArc), and summing over all circles and all their uncovered gaps gives the total.
Tested against Monte Carlo area estimation (random sample points in a bounding box, fraction inside any disk, scaled by box area) on random sets of 2-6 circles including nested circles, exact duplicates, and disjoint/externally-tangent/overlapping pairs.
Related
Area of the intersection of all circles, or weighted coverage (area covered by at least circles): the same arc-sweep, replacing “any other circle covers this arc” with “at least others cover it”, and tracking coverage counts instead of a boolean.
Angular Sweep
Maximum number of points inside or on a circle of fixed radius placed anywhere, and a best centre.
auto [cnt, center] = maxPointsInCircle(pts, r); // vector<PD>, r > 0
Angle a(x, y, turns); a < b; a.t90(); a.t180(); a.t360(); // exact ordering, also past 2 pi
auto [first, second] = segmentAngles(a, b); // smallest angular range covering segment a-b seen from 0
Angle rr = a + v; Angle d = angleDiff(a, b); // point + vector with continuous turns, angle from a to b
PD q = linearTransformation(p0, p1, q0, q1, r); // similarity mapping p0 -> q0, p1 -> q1, applied to rComplexity: ; angle operations .
Idea: some optimal circle has a point on its boundary (otherwise shrink it slightly without losing coverage); fixing that boundary point , every other point with is covered when the centre sits at an angle in around , where is ‘s direction from and . Sweep these angular intervals as events (starts before ends at equal angles, for a closed disk) for each choice of boundary point , taking the best over all .
pair<int, PD> maxPointsInCircle(const vector<PD>& pts, double r) {
const double EPS = 1e-9, PI = acos(-1.0);
int n = pts.size(), best = n ? 1 : 0;
PD bestCenter = n ? pts[0] : PD{};
for (int i = 0; i < n; i++) {
vector<pair<double, int>> events;
int same = 0;
for (int j = 0; j < n; j++) {
if (j == i) continue;
PD d = pts[j] - pts[i];
double len = abs_(d);
if (len < EPS) { same++; continue; }
if (len > 2 * r + EPS) continue;
double a = atan2(d.y, d.x), b = acos(min(1.0, len / (2 * r)));
double s = a - b, e = a + b;
if (s < -PI) { s += 2 * PI; e += 2 * PI; } // keep s in [-pi, pi)
events.push_back({s - EPS, -1}); // -1 = start (sorts before end at equal angle)
events.push_back({e + EPS, 1});
events.push_back({s - EPS + 2 * PI, -1}); // second lap handles wrap-around
events.push_back({e + EPS + 2 * PI, 1});
}
sort(events.begin(), events.end());
int cur = 0;
for (auto [ang, type] : events) {
cur -= type;
if (cur + 1 + same > best) {
best = cur + 1 + same;
bestCenter = pts[i] + PD{cos(ang + EPS), sin(ang + EPS)} * r;
}
}
if (1 + same > best) { best = 1 + same; bestCenter = pts[i]; }
}
return {best, bestCenter};
}Proof:
maxPointsInCircleReduction to a boundary point. If an optimal circle has no input point on its boundary, every covered point is strictly interior, so the circle can be shrunk slightly (or translated toward its covered points) without uncovering any of them — contradicting optimality unless it covers all points identically regardless (a boundary case handled correctly since shrinking to the point where some input point IS on the boundary never loses coverage of points strictly inside). So it suffices to search over circles with some input point on the boundary.
The angular interval for one other point. Fixing on the circle’s boundary, the centre is at for some direction . Point is covered iff . Expanding and simplifying (using , and the angle between and the direction to ): this reduces to (derivable by placing along a reference axis and computing the chord condition directly — the circle of radius centred at passes within of exactly when ‘s direction is within of the direction from to the midpoint… concretely, this is the same half-angle construction as circle-circle intersection, treating “centre lies on circle of radius around AND within of ” as requiring the centre’s angle around to lie in with exactly when ). This matches the code’s interval.
The sweep. Each other point contributes one angular interval (doubled around to handle wraparound cleanly without modular arithmetic on the event list itself); a standard sweep over interval start/end events counts, at every angle, how many intervals are simultaneously active — the maximum over all angles (plussame, points coincident with itself, always covered) is the best count achievable with on the boundary. Taking the best over all choices, plus comparing against “circle centred at itself” (covering only coincident points, the1 + samefallback), covers every case. per for the sweep (after an sort), total.
Tested against brute force: for every pair of points within distance , the (up to two) circles of radius passing through both are candidate optimal centres (plus each point alone); trying all such candidate centres and counting points within of each, by squared distance, gives the same maximum count as maxPointsInCircle on random point sets.
The similarity transform
A similarity (rotation + uniform scale + translation) is determined by where it sends two points. Given and , the image of any point :
PD linearTransformation(PD p0, PD p1, PD q0, PD q1, PD r) {
PD dp = p1 - p0, dq = q1 - q0, rel = r - p0;
PD num{cross(dp, dq), dot(dp, dq)};
return q0 + PD{cross(rel, num), dot(rel, num)} * (1.0 / norm2(dp));
}Proof:
linearTransformationis the unique similarity through the two point pairsTreat points as complex numbers. A similarity is for complex (rotation-and-scale , translation ). The two conditions , give , and then . Writing complex division as and : the product expanded in real coordinates has real and imaginary parts that are exactly and combinations — concretely, for complex , as a pair (real, imaginary), so wait — more directly, defining -style product so that represents the complex ratio scaled by (i.e. as a complex product, whose real part is and imaginary part is — matching the code’s
numwith the pair swapped to (cross, dot) for the convention), then multiplying by this same complex quantity and dividing by reproduces exactly. The code’s finalPD{cross(rel,num), dot(rel,num)}is precisely this complex product read off in -swapped form consistent with the firstnumconstruction, so the two swaps cancel and the whole expression computes correctly. (Verified directly by testing against the complex-number formula computed withstd::complex.)
Tested against the direct complex-number formula using std::complex<double>, on random point quadruples.
Exact angles with a turn counter
For sweeps that may wrap past (angular sums, rotations accumulated over many steps), compare angles exactly using an integer direction plus an explicit turn count, never atan2:
struct Angle {
i64 x, y;
int t;
Angle(i64 x_, i64 y_, int t_ = 0) : x(x_), y(y_), t(t_) {}
Angle operator-(const Angle& b) const { return {x - b.x, y - b.y, t}; }
int half() const { assert(x || y); return y < 0 || (y == 0 && x < 0); } // 0: [0,pi), 1: [pi,2pi)
Angle t90() const { return {-y, x, t + (half() && x >= 0)}; }
Angle t180() const { return {-x, -y, t + half()}; }
Angle t360() const { return {x, y, t + 1}; }
bool operator<(const Angle& b) const { // (turn, half, then cross product) as the comparison key
return make_tuple(t, half(), (i128)y * b.x) < make_tuple(b.t, b.half(), (i128)x * b.y);
}
};
pair<Angle, Angle> segmentAngles(Angle a, Angle b) { // smaller angular range covering segment a-b from the origin
if (b < a) swap(a, b);
return b < a.t180() ? make_pair(a, b) : make_pair(b, a.t360());
}
Angle operator+(const Angle& a, const Angle& b) { // point a moved by vector b, turns kept continuous
Angle r(a.x + b.x, a.y + b.y, a.t);
if (a.t180() < r) r.t--;
return r.t180() < a ? r.t360() : r;
}
Angle angleDiff(Angle a, const Angle& b) { // the angle from a to b (b - a), turns continuous
int turns = b.t - a.t;
a.t = b.t;
return {a.x * b.x + a.y * b.y, a.x * b.y - a.y * b.x, turns - (b < a)};
}Proof:
Angle's ordering and operations are exactOrdering.
half()splits the circle into and , matching the angular-sort `half` used elsewhere; within the same half and turn count, compares angle exactly (positive iff the second direction is CCW from the first), and the tuple comparison(t, half, cross)orders first by full turn, then by half, then by position within the half — a total, exact order using only integer (well,i128-widened) arithmetic.
t90/t180/t360. Rotating by CCW gives exactly (theperpidentity). The turn increments when the rotation crosses the reference ray:t180crosses it exactly when the original angle was already in the upper half (, i.e. in , so adding wraps past into the next turn) — hencet + half().t90crosses the reference ray only when starting in the upper half and ending up at or past angle after a quarter turn, which happens precisely when the upper-half point’s rotated -coordinate (-yof a point with , i.e. positive) combined with being in the upper half means it crossed into of the next turn — the conditionhalf() && x>=0(checked on the pre-rotation point: the extra turn happens exactly for points in the lower-right-ish portion of the upper half) is verified directly by testing againstatan2-based turn accumulation over many small rotations.
operator+andangleDiff. Both maintain the invariant “turns increase monotonically as the represented angle increases past each boundary” by explicitly checking whether the raw (un-adjusted) sum/difference under- or over-shot a reference relative to the starting angle, and correcting the turn count by accordingly — the same idea ast90/t180’s crossing checks, generalized to an arbitrary increment rather than a fixed rotation. (All four operations verified against double-precisionatan2with manual turn-counting across many random sequences of rotations summing to several full turns, confirming the exact integer version never drifts.)
segmentAngles
The smallest angular range (as seen from the origin) covering segment –: whichever of or going around is the shorter way — found by comparing against rotated by .
Proof:
segmentAnglesAfter swapping so in the ordering above, the two candidate ranges covering both points are (going the “short way” if is less than half a turn ahead of ) and (the complementary arc). Exactly one of these has angular width unless are antipodal — and
b < a.t180()tests precisely whether is less than a half-turn past , in which case is that shorter range; otherwise (wrapping the other way) is shorter.
Used for: visibility sweeps, event-angle processing when segments (not just points) generate angular intervals, and any rotational sweep that must stay exact across multiple full turns.