Skip to Content

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 n105n \leq 10^5 puntos pi=(xi,yi)p_i=(x_i, y_i).

Para cada pip_i, determinar si yace sobre la circunferencia del círculo mínimo envolvente de {p1,,pn}{p_1,\dots,p_n}.

Aquí, por círculo mínimo envolvente (MEC, minimum enclosing circle) entendemos un círculo de radio lo menor posible que contiene los nn 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 O(n3)O(n^3), pero en realidad funciona en tiempo esperado O(n)O(n).

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 rr el radio del MEC. Dibujamos un círculo de radio rr alrededor de cada uno de los puntos p1,,pnp_1,\dots,p_n. Geométricamente, los centros de los círculos que tienen radio rr y cubren todos los puntos p1,,pnp_1,\dots,p_n forman la intersección de los nn 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 rr un poquito y aún tener intersección no vacía, lo que contradice la hipótesis de que rr 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 pip_i o por dos puntos pip_i y pjp_j (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 p1,,pnp_1,\dots,p_n) 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 mec(p1,,pn)\operatorname{mec}(p_1,\dots,p_n) al MEC de {p1,,pn}{p_1,\dots,p_n}, y sea Pi={p1,,pi}P_i = {p_1,\dots,p_i}.

El algoritmo, propuesto  inicialmente por Welzl en 1991, procede como sigue:

  1. Aplicar una permutación aleatoria a la secuencia de entrada de puntos.
  2. Mantener el candidato actual a ser el MEC CC, empezando con C=mec(p1,p2)C = \operatorname{mec}(p_1, p_2).
  3. Iterar sobre i=3..ni=3..n y comprobar si piCp_i \in C.
    1. Si piCp_i \in C significa que CC es el MEC de PiP_i.
    2. En caso contrario, asignar C=mec(pi,p1)C = \operatorname{mec}(p_i, p_1) e iterar sobre j=2..ij=2..i y comprobar si pjCp_j \in C.
      1. Si pjCp_j \in C, entonces CC es el MEC de PjP_j entre los círculos que pasan por pip_i.
      2. En caso contrario, asignar C=mec(pi,pj)C=\operatorname{mec}(p_i, p_j) e iterar sobre k=1..jk=1..j y comprobar si pkCp_k \in C.
        1. Si pkCp_k \in C, entonces CC es el MEC de PkP_k entre los círculos que pasan por pip_i y pjp_j.
        2. En caso contrario, C=mec(pi,pj,pk)C=\operatorname{mec}(p_i,p_j,p_k) es el MEC de PkP_k entre los círculos que pasan por pip_i y pjp_j.

Se puede ver que cada nivel de anidamiento aquí tiene un invariante que mantener (que CC es el MEC entre los círculos que además pasan por 00, 11 o 22 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 pip_i está dentro del MEC de 22 o 33 puntos se pueda hacer en O(1)O(1) (lo discutiremos más adelante). Pero incluso entonces, el algoritmo de arriba parece que tomaría O(n3)O(n^3) 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 kk), claramente su tiempo esperado es O(j)O(j) operaciones. ¿Y el bucle sobre jj?

Solo dispara el siguiente bucle si pjp_j está en la frontera del MEC de PjP_j que además pasa por el punto ii, y quitar pjp_j encogería aún más el círculo. De todos los puntos de PjP_j solo puede haber a lo sumo 22 puntos con esa propiedad, porque si hay más de 22 puntos de PjP_j en la frontera, significa que después de quitar cualquiera de ellos aún habrá al menos 33 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 2j\frac{2}{j} de que obtengamos uno de los a lo sumo dos puntos desafortunados como pjp_j. Sumando sobre todos los jj de 11 a ii, obtenemos el tiempo esperado de

j=1i2jO(j)=O(i). \sum\limits_{j=1}^i \frac{2}{j} \cdot O(j) = O(i).

De exactamente la misma manera ahora también podemos demostrar que el bucle más externo tiene tiempo esperado O(n)O(n).

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 x+yix+yi, donde i2=1i^2=-1 y x,yRx, y \in \mathbb R. En C++, tal número complejo se representa por un punto bidimensional (x,y)(x, y). 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 OxOx en sentido antihorario), y tomar el conjugado (es decir, cambiar z=x+yiz=x+yi por z=xyi\overline{z} = x-yi) multiplica el ángulo polar por 1-1. Esto nos permite formular algunos criterios muy simples para si un punto zz está dentro del MEC de 22 o 33 puntos específicos.

MEC de 2 puntos

Para 22 puntos aa y bb, su MEC es simplemente el círculo centrado en a+b2\frac{a+b}{2} con radio ab2\frac{|a-b|}{2}, en otras palabras el círculo que tiene abab como diámetro. Para comprobar si zz está dentro de este círculo simplemente hay que comprobar que el ángulo entre zaza y zbzb 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

I0=(bz)(az) I_0=(b-z)\overline{(a-z)}

no tiene una coordenada real positiva (correspondiente a puntos que tienen un ángulo polar entre 90-90^\circ y 9090^\circ).

MEC de 3 puntos

Añadir zz al triángulo abcabc lo convertirá en un cuadrilátero. Considérese la siguiente expresión:

azb+bca \angle azb + \angle bca

En un cuadrilátero cíclico , si cc y zz están del mismo lado de abab, entonces los ángulos son iguales, y sumarán 00^\circ cuando se sumen con signo (es decir, positivos si son antihorarios y negativos si son horarios). Correspondientemente, si cc y zz están en lados opuestos, los ángulos sumarán 180180^\circ.


Los ángulos inscritos adyacentes son iguales; los ángulos opuestos se complementan a 180 grados

En términos de números complejos, podemos notar que azb\angle azb es el ángulo polar de (bz)(az)(b-z)\overline{(a-z)} y bca\angle bca es el ángulo polar de (ac)(bc)(a-c)\overline{(b-c)}. Así, podemos concluir que azb+bca\angle azb + \angle bca es el ángulo polar de

I1=(bz)(az)(ac)(bc) I_1 = (b-z) \overline{(a-z)} (a-c) \overline{(b-c)}

Si el ángulo es 00^\circ o 180180^\circ, significa que la parte imaginaria de I1I_1 es 00; en caso contrario podemos deducir si zz está dentro o fuera del círculo envolvente de abcabc comprobando el signo de la parte imaginaria de I1I_1. La parte imaginaria positiva corresponde a ángulos positivos, y la parte imaginaria negativa corresponde a ángulos negativos.

Pero ¿cuál de ellos significa que zz está dentro o fuera del círculo? Como ya notamos, tener zz dentro del círculo generalmente aumenta la magnitud de azb\angle azb, mientras que tenerlo fuera del círculo la disminuye. Así, tenemos los siguientes 4 casos:

  1. bca>0\angle bca > 0^\circ, cc del mismo lado de abab que zz. Entonces, azb<0\angle azb < 0^\circ, y azb+bca<0\angle azb + \angle bca < 0^\circ para puntos dentro del círculo.
  2. bca<0\angle bca < 0^\circ, cc del mismo lado de abab que zz. Entonces, azb>0\angle azb > 0^\circ, y azb+bca>0\angle azb + \angle bca > 0^\circ para puntos dentro del círculo.
  3. bca>0\angle bca > 0^\circ, cc del lado opuesto de abab respecto de zz. Entonces, azb>0\angle azb > 0^\circ y azb+bca>180\angle azb + \angle bca > 180^\circ para puntos dentro del círculo.
  4. bca<0\angle bca < 0^\circ, cc del lado opuesto de abab respecto de zz. Entonces, azb<0\angle azb < 0^\circ y azb+bca<180\angle azb + \angle bca < 180^\circ para puntos dentro del círculo.

En otras palabras, si bca\angle bca es positivo, los puntos dentro del círculo tendrán azb+bca<0\angle azb + \angle bca < 0^\circ; en caso contrario tendrán azb+bca>0\angle azb + \angle bca > 0^\circ, asumiendo que normalizamos los ángulos entre 180-180^\circ y 180180^\circ. Esto, a su vez, se puede comprobar por los signos de las partes imaginarias de I2=(ac)(bc)I_2=(a-c)\overline{(b-c)} y I1=I0I2I_1 = I_0 I_2.

Nota: Como multiplicamos cuatro números complejos para obtener I1I_1, los coeficientes intermedios pueden ser tan grandes como O(A4)O(A^4), donde AA 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 .

Problemas de práctica