| tags |
|
|---|
Consider the following problem:
!!! example "Library Checker - Minimum Enclosing Circle"
You're given $n \leq 10^5$ points $p_i=(x_i, y_i)$.
For each $p_i$, find whether it lies on the circumference of the minimum enclosing circle of $\{p_1,\dots,p_n\}$.
Here, by the minimum enclosing circle (MEC) we mean a circle with minimum possible radius that contains all the
To better understand the reasoning below, we should immediately note that the solution to the problem is unique:
??? question "Why is the MEC unique?"
Consider the following setup: Let $r$ be the radius of the MEC. We draw a circle of radius $r$ around each of the points $p_1,\dots,p_n$. Geometrically, the centers of circles that have radius $r$ and cover all the points $p_1,\dots,p_n$ form the intersection of all $n$ circles.
Now, if the intersection is just a single point, this already proves that it is unique. Otherwise, the intersection is a shape of non-zero area, so we can reduce $r$ by a tiny bit, and still have non-empty intersection, which contradicts the assumption that $r$ was the minimum possible radius of the enclosing circle.
With a similar logic, we can also show the uniqueness of the MEC if we additionally demand that it passes through a given specific point $p_i$ or two points $p_i$ and $p_j$ (it is also unique because its radius uniquely defines it).
Alternatively, we can also assume that there are two MECs, and then notice that their intersection (which contains the points $p_1,\dots,p_n$ already) must have a smaller diameter than initial circles, and thus can be covered with a smaller circle.
For brevity, let's denote
The algorithm, initially proposed by Welzl in 1991, goes as follows:
- Apply a random permutation to the input sequence of points.
- Maintain the current candidate to be the MEC
$C$ , starting with$C = \operatorname{mec}(p_1, p_2)$ . - Iterate over
$i=3..n$ and check if$p_i \in C$ .- If
$p_i \in C$ it means that$C$ is the MEC of$P_i$ . - Otherwise, assign
$C = \operatorname{mec}(p_i, p_1)$ and iterate over$j=2..i$ and check if$p_j \in C$ .- If
$p_j \in C$ , then$C$ is the MEC of$P_j$ among circles that pass through$p_i$ . - Otherwise, assign
$C=\operatorname{mec}(p_i, p_j)$ and iterate over$k=1..j$ and check if$p_k \in C$ .- If
$p_k \in C$ , then$C$ is the MEC of$P_k$ among circles that pass through$p_i$ and$p_j$ . - Otherwise,
$C=\operatorname{mec}(p_i,p_j,p_k)$ is the MEC of$P_k$ among circles that pass through$p_i$ and$p_j$ .
- If
- If
- If
We can see that each level of nestedness here has an invariant to maintain (that
Omitting some technical details, for now, the whole algorithm can be implemented in C++ as follows:
struct point {...};
// Is represented by 2 or 3 points on its circumference
struct mec {...};
bool inside(mec const& C, point p) {
return ...;
}
// Choose some good generator of randomness for the shuffle
mt19937_64 gen(...);
mec enclosing_circle(vector<point> &p) {
int n = p.size();
ranges::shuffle(p, gen);
auto C = mec{p[0], p[1]};
for(int i = 0; i < n; i++) {
if(!inside(C, p[i])) {
C = mec{p[i], p[0]};
for(int j = 0; j < i; j++) {
if(!inside(C, p[j])) {
C = mec{p[i], p[j]};
for(int k = 0; k < j; k++) {
if(!inside(C, p[k])) {
C = mec{p[i], p[j], p[k]};
}
}
}
}
}
}
return C;
}Now, it is to be expected that checking that a point
For the inner-most loop (over
It only triggers the next loop if
In other words, after initial random shuffle, there is at most
In exactly same fashion we can now also prove that the outermost loop has expected runtime of
Let's now figure out the implementation detail of point and mec. In this problem, it turns out to be particularly useful to use std::complex as a class for points:
using ftype = int64_t;
using point = complex<ftype>;As a reminder, a complex number is a number of type
Without going in too much detail, we will note the most important property for this particular task: Multiplying two complex numbers adds up their polar angles (counted from
For
Inner angles are obtuse, external angles are acute and angles on the circumference are right
Equivalently, we need to check that
doesn't have a positive real coordinate (corresponding to points that have a polar angle between
Adding
In a cyclic quadrilateral, if
Adjacent inscribed angles are same, opposing angles complement to 180 degrees
In terms of complex numbers, we can note that
If the angle is
But which one of them means that
-
$\angle bca > 0^\circ$ ,$c$ on the same side of$ab$ as$z$ . Then,$\angle azb < 0^\circ$ , and$\angle azb + \angle bca < 0^\circ$ for points inside the circle. -
$\angle bca < 0^\circ$ ,$c$ on the same side of$ab$ as$z$ . Then,$\angle azb > 0^\circ$ , and$\angle azb + \angle bca > 0^\circ$ for points inside the circle. -
$\angle bca > 0^\circ$ ,$c$ on the opposite side of$ab$ to$z$ . Then,$\angle azb > 0^\circ$ and$\angle azb + \angle bca > 180^\circ$ for points inside the circle. -
$\angle bca < 0^\circ$ ,$c$ on the opposite side of$ab$ to$z$ . Then,$\angle azb < 0^\circ$ and$\angle azb + \angle bca < 180^\circ$ for points inside the circle.
In other words, if
Note: As we multiply four complex numbers to get
Now, to actually implement the check, we should first decide how to represent the MEC. As our criteria work with the points directly, a natural and efficient way to do this is to say that MEC is directly represented as a pair or triple of points that defines it:
using mec = variant<
array<point, 2>,
array<point, 3>
>;Now, we can use std::visit to efficiently deal with both cases in accordance with criteria above:
/* I < 0 if z inside C,
I > 0 if z outside C,
I = 0 if z on the circumference of C */
ftype indicator(mec const& C, point z) {
return visit([&](auto &&C) {
point a = C[0], b = C[1];
point I0 = (b - z) * conj(a - z);
if constexpr (size(C) == 2) {
return real(I0);
} else {
point c = C[2];
point I2 = (a - c) * conj(b - c);
point I1 = I0 * I2;
return imag(I2) < 0 ? -imag(I1) : imag(I1);
}
}, C);
}
bool inside(mec const& C, point p) {
return indicator(C, p) <= 0;
}
Now, we can finally ensure that everything works by submitting the problem to the Library Checker: #308668.