Círculo mínimo envolvente
Considérese el siguiente problema:
[Library Checker - Minimum Enclosing Circle](https://judge.yosupo.jp/problem/minimum_enclosing_circle)
Se dan puntos .
Para cada , determinar si yace sobre la circunferencia del círculo mínimo envolvente de .
Aquí, por círculo mínimo envolvente (MEC, minimum enclosing circle) entendemos un círculo de radio lo menor posible que contiene los puntos, dentro del círculo o sobre su frontera. Este problema tiene una solución aleatorizada simple que, a primera vista, parece que correría en , pero en realidad funciona en tiempo esperado .
Para entender mejor el razonamiento de abajo, debemos notar de inmediato que la solución del problema es única:
¿Por qué el MEC es único?
Considérese el siguiente planteo: Sea el radio del MEC. Dibujamos un círculo de radio alrededor de cada uno de los puntos . Geométricamente, los centros de los círculos que tienen radio y cubren todos los puntos forman la intersección de los círculos.
Ahora, si la intersección es un solo punto, esto ya demuestra que es única. En caso contrario, la intersección es una forma de área no nula, de modo que podemos reducir un poquito y aún tener intersección no vacía, lo que contradice la hipótesis de que era el menor radio posible del círculo envolvente.
Con una lógica similar, también podemos mostrar la unicidad del MEC si además exigimos que pase por un punto específico dado o por dos puntos y (también es único porque su radio lo define de forma única).
Alternativamente, también podemos asumir que hay dos MEC, y luego notar que su intersección (que ya contiene los puntos ) debe tener un diámetro menor que los círculos iniciales, y por tanto se puede cubrir con un círculo más pequeño.
Algoritmo de Welzl
Por brevedad, denotemos al MEC de , y sea .
El algoritmo, propuesto inicialmente por Welzl en 1991, procede como sigue:
- Aplicar una permutación aleatoria a la secuencia de entrada de puntos.
- Mantener el candidato actual a ser el MEC , empezando con .
- Iterar sobre y comprobar si .
- Si significa que es el MEC de .
- En caso contrario, asignar e iterar sobre y comprobar si .
- Si , entonces es el MEC de entre los círculos que pasan por .
- En caso contrario, asignar e iterar sobre y comprobar si .
- Si , entonces es el MEC de entre los círculos que pasan por y .
- En caso contrario, es el MEC de entre los círculos que pasan por y .
Se puede ver que cada nivel de anidamiento aquí tiene un invariante que mantener (que es el MEC entre los círculos que además pasan por , o puntos dados adicionalmente), y cada vez que se cierra el bucle interno, su invariante se vuelve equivalente al invariante de la iteración actual de su bucle padre. Esto, a su vez, asegura la corrección del algoritmo en su conjunto.
Omitiendo algunos detalles técnicos, por ahora, el algoritmo completo se puede implementar en C++ como sigue:
struct point {...};
// Se representa por 2 o 3 puntos sobre su circunferencia
struct mec {...};
bool inside(mec const& C, point p) {
return ...;
}
// Elegir algún generador de aleatoriedad bueno para el 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;
}Ahora, es de esperar que comprobar que un punto está dentro del MEC de o puntos se pueda hacer en (lo discutiremos más adelante). Pero incluso entonces, el algoritmo de arriba parece que tomaría en el peor caso solo por todos los bucles anidados. Entonces, ¿cómo es que afirmamos el tiempo esperado lineal? ¡Veámoslo!
Análisis de complejidad
Para el bucle más interno (sobre ), claramente su tiempo esperado es operaciones. ¿Y el bucle sobre ?
Solo dispara el siguiente bucle si está en la frontera del MEC de que además pasa por el punto , y quitar encogería aún más el círculo. De todos los puntos de solo puede haber a lo sumo puntos con esa propiedad, porque si hay más de puntos de en la frontera, significa que después de quitar cualquiera de ellos aún habrá al menos puntos en la frontera, suficientes para definir el círculo de forma única.
En otras palabras, después de la mezcla aleatoria inicial, hay a lo sumo probabilidad de que obtengamos uno de los a lo sumo dos puntos desafortunados como . Sumando sobre todos los de a , obtenemos el tiempo esperado de
De exactamente la misma manera ahora también podemos demostrar que el bucle más externo tiene tiempo esperado .
Comprobar que un punto está en el MEC de 2 o 3 puntos
Ahora veamos el detalle de implementación de point y mec. En este problema resulta particularmente útil usar std::complex como clase para los puntos:
using ftype = int64_t;
using point = complex<ftype>;Como recordatorio, un número complejo es un número del tipo , donde y . En C++, tal número complejo se representa por un punto bidimensional . Los números complejos ya implementan operaciones lineales básicas componente a componente (suma, multiplicación por un número real), pero también su multiplicación y división tienen cierto significado geométrico.
Sin entrar en demasiado detalle, notaremos la propiedad más importante para esta tarea en particular: multiplicar dos números complejos suma sus ángulos polares (contados desde en sentido antihorario), y tomar el conjugado (es decir, cambiar por ) multiplica el ángulo polar por . Esto nos permite formular algunos criterios muy simples para si un punto está dentro del MEC de o puntos específicos.
MEC de 2 puntos
Para puntos y , su MEC es simplemente el círculo centrado en con radio , en otras palabras el círculo que tiene como diámetro. Para comprobar si está dentro de este círculo simplemente hay que comprobar que el ángulo entre y no es agudo.
Los ángulos interiores son obtusos, los ángulos exteriores son agudos y los ángulos sobre la circunferencia son rectos
De forma equivalente, hay que comprobar que
no tiene una coordenada real positiva (correspondiente a puntos que tienen un ángulo polar entre y ).
MEC de 3 puntos
Añadir al triángulo lo convertirá en un cuadrilátero. Considérese la siguiente expresión:
En un cuadrilátero cíclico , si y están del mismo lado de , entonces los ángulos son iguales, y sumarán cuando se sumen con signo (es decir, positivos si son antihorarios y negativos si son horarios). Correspondientemente, si y están en lados opuestos, los ángulos sumarán .
Los ángulos inscritos adyacentes son iguales; los ángulos opuestos se complementan a 180 grados
En términos de números complejos, podemos notar que es el ángulo polar de y es el ángulo polar de . Así, podemos concluir que es el ángulo polar de
Si el ángulo es o , significa que la parte imaginaria de es ; en caso contrario podemos deducir si está dentro o fuera del círculo envolvente de comprobando el signo de la parte imaginaria de . La parte imaginaria positiva corresponde a ángulos positivos, y la parte imaginaria negativa corresponde a ángulos negativos.
Pero ¿cuál de ellos significa que está dentro o fuera del círculo? Como ya notamos, tener dentro del círculo generalmente aumenta la magnitud de , mientras que tenerlo fuera del círculo la disminuye. Así, tenemos los siguientes 4 casos:
- , del mismo lado de que . Entonces, , y para puntos dentro del círculo.
- , del mismo lado de que . Entonces, , y para puntos dentro del círculo.
- , del lado opuesto de respecto de . Entonces, y para puntos dentro del círculo.
- , del lado opuesto de respecto de . Entonces, y para puntos dentro del círculo.
En otras palabras, si es positivo, los puntos dentro del círculo tendrán ; en caso contrario tendrán , asumiendo que normalizamos los ángulos entre y . Esto, a su vez, se puede comprobar por los signos de las partes imaginarias de y .
Nota: Como multiplicamos cuatro números complejos para obtener , los coeficientes intermedios pueden ser tan grandes como , donde es la mayor magnitud de coordenada en la entrada. Del lado positivo, si la entrada es entera, ambas comprobaciones de arriba se pueden hacer enteramente en enteros.
Implementación
Ahora, para implementar realmente la comprobación, primero debemos decidir cómo representar el MEC. Como nuestros criterios trabajan con los puntos directamente, una forma natural y eficiente de hacerlo es decir que el MEC se representa directamente como un par o una terna de puntos que lo define:
using mec = variant<
array<point, 2>,
array<point, 3>
>;Ahora, podemos usar std::visit para tratar ambos casos de forma eficiente de acuerdo con los criterios de arriba:
/* I < 0 si z está dentro de C,
I > 0 si z está fuera de C,
I = 0 si z está sobre la circunferencia de 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;
}
Ahora, por fin podemos asegurar que todo funciona enviando el problema al Library Checker: #308668 .