Envolvente convexa
Introducción
La envolvente convexa (Convex Hull) es el subconjunto de puntos que forma el polígono convexo más pequeño que encierra a todos los puntos del conjunto. Para visualizarlo, imaginar que cada punto es un poste. Después, imaginar qué pasa si se envuelve una soga por fuera de todos los postes y se tira con fuerza infinita, de modo que las conexiones entre cualesquiera dos puntos que quedan sobre el borde de la soga son segmentos. El conjunto de puntos que tocan la soga es la envolvente convexa.
Visualización de la envolvente convexa

| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| Kattis | Convex Hull | Fácil | Convex | en el módulo |
Con Graham Scan
| Fuente | Recurso | Notas |
|---|---|---|
| Wikipedia | Graham Scan | |
| VisuAlgo | Graham Scan Visualization | |
| UCSD | Original Graham Scan Paper |
Video de YouTube (9rQMLpQn5xQ)
Solución
| Fuente | Recurso | Notas |
|---|---|---|
| Benq | Graham Scan Implementation |
El algoritmo de Graham Scan funciona en 3 pasos. Primero, ordena los puntos por su ángulo antihorario alrededor de un pivote , desempatando por distancia. Este algoritmo usa como el punto más a la izquierda (y el más abajo si hay empate).
Mantenemos una pila que contiene los puntos de modo que se cumple el siguiente invariante: cada tres puntos consecutivos de la pila forman un giro antihorario. En otras palabras, queda a la izquierda de la recta de a . Esta condición implica que los puntos de la pila forman los vértices de un polígono convexo.
Para empezar a construir la envolvente convexa, elegimos 2 puntos. El pivote (primer punto) y el segundo punto según el ordenamiento inicial. Después de eso, intentamos agregar a la pila cada punto de .
Denotemos nuestra pila como , el elemento tope de como y como el -ésimo punto candidato ordenado. Antes de agregar a la pila, chequeamos si
forma un giro antihorario.
- Si es así, entonces agregamos a la pila y el invariante se mantiene. Seguimos con .
- Si no, queda dentro de la envolvente convexa de los demás puntos de la pila junto con , así que hacemos pop de de la pila y seguimos con .
Ilustración

Ejemplo resuelto
Hallar la envolvente convexa de los puntos .
Usando los pasos de nuestro Graham Scan:
-
Ordenar los puntos por ángulo antihorario: nuestro punto más a la izquierda es , así que ordenamos todos los puntos por su ángulo antihorario. Si los ángulos coinciden, desempatamos por distancia. Los puntos ordenados resultantes son con pivote inicial . Notar que y son colineales, pero como está más lejos desempata y queda antes que .
-
Mantener la pila:
Los elementos iniciales de la pila son . Después, empezando desde , procesamos cada punto.
Primero, chequeamos si el giro entre , y es antihorario, y lo es. Por lo tanto, el punto se inserta en la pila.
La pila ahora corresponde a los puntos: .
Después, chequeamos el giro , que es horario. Por lo tanto, hacemos pop de la pila hasta que se pueda satisfacer la propiedad antihoraria. En este caso, solo se saca . Después, hacemos push de a la pila porque satisface la propiedad antihoraria.
La pila ahora corresponde a los puntos: .
Después, chequeamos si el giro entre , , es antihorario, y lo es, así que hacemos push de a la pila.
La pila ahora corresponde a los puntos: .
Después, chequeamos si el giro entre es antihorario, y lo es, así que hacemos push de a la pila.
Como ya procesamos todos los puntos, completamos el ciclo y la envolvente convexa está lista.
La pila queda entonces , que son los vértices de la envolvente convexa.
Otra visualización de envolvente convexa con más puntos se puede ver acá (creada con GeoDeb ).
#include <bits/stdc++.h>
using namespace std;
#define FOR(i, a, b) for (int i = (a); i < (b); i++)
#define FORE(i, a, b) for (int i = (a); i <= (b); i++)
#define F0R(i, a) for (int i = 0; i < (a); i++)
#define trav(a, x) for (auto &a : x)
#define f first
#define s second
#define bk back()
#define pb push_back
#define sz(x) int((x).size())
#define bg(x) begin(x)
#define all(x) bg(x), end(x)
int N, Q;
const int MX = 2e5 + 5;
using T = long long; // or long long
const T EPS = 1e-9; // might want to change
using P = pair<T, T>;
using vP = vector<P>;
using Line = pair<P, P>;
T sq(T a) { return a * a; } // square
T norm(const P &p) { return sq(p.f) + sq(p.s); } // x^2 + y^2
// basic operations
P operator-(const P &l, const P &r) { return P(l.f - r.f, l.s - r.s); }
T cross(const P &a, const P &b) { return a.f * b.s - a.s * b.f; } // cross product
T cross(const P &p, const P &a, const P &b) { // cross product
return cross(a - p, b - p);
}
using vi = vector<int>;
using vP = vector<P>;
vi hullInd(const vP &v) {
int ind = int(min_element(all(v)) - begin(v));
vi cand, hull{ind};
F0R(i, sz(v)) if (v[i] != v[ind]) cand.pb(i);
sort(all(cand), [&](int a, int b) { // sort by angle, tiebreak by distance
P x = v[a] - v[ind], y = v[b] - v[ind];
T t = cross(x, y);
return t != 0 ? t > 0 : norm(x) < norm(y);
});
trav(c, cand) { // for every point
while (sz(hull) > 1 && cross(v[end(hull)[-2]], v[hull.bk], v[c]) <= 0) {
hull.pop_back(); // pop until counterclockwise and size > 1
}
hull.pb(c);
}
return hull;
}
int main() {
ios_base::sync_with_stdio(0);
cin.tie(0);
for (;;) {
int n;
cin >> n;
if (!n) break;
vP pts(n);
F0R(i, n) {
long long a, b;
cin >> a >> b;
pts[i] = P(a, b);
}
vi ret = hullInd(pts); // gets hull indices of convex hull
cout << sz(ret) << '\n';
trav(a, ret) { cout << pts[a].first << " " << pts[a].second << '\n'; }
}
}Con cadena monótona
| Fuente | Recurso | Notas |
|---|---|---|
| CPH | Monotone Chain | |
| Wikipedia | Monotone Chain | |
| Benq | Monotone Chain Implementation |
Solución
Con el algoritmo de cadena monótona (Monotone Chain), empezamos ordenando los puntos dados en orden creciente respecto de sus coordenadas . Si dos puntos tienen la misma coordenada , entonces miramos la coordenada .
Después, calculamos la envolvente convexa en dos partes: la envolvente superior y la envolvente inferior. Primero, observamos que los puntos inicial y final de ambas envolventes (superior e inferior) son los mismos. Son los puntos con menor y mayor valor de respectivamente, y . Empezamos agregando y a la envolvente. (Notar que no tiene por qué quedar en la envolvente convexa al final). Después, empezando desde , iteramos por los puntos ordenados y los agregamos a la envolvente. Denotemos el punto actual que se está agregando como y el último punto que sigue en la envolvente como . Al agregar puntos nuevos, queremos asegurarnos de que no hay giro a la derecha entre todos los segmentos de la envolvente, igual que en el algoritmo de Graham Scan de arriba. Para lograrlo, el segmento siempre debería quedar a la derecha del segmento . Esto se puede calcular usando un producto cruz:
- Si , el punto queda a la izquierda del segmento . En este caso, hay que sacar el punto de la envolvente y repetir este chequeo.
- Si , el punto queda sobre el segmento . Si incluir varios puntos colineales depende del enunciado, pero para el problema de arriba también sacamos el punto y repetimos el chequeo.
- En caso contrario, el punto queda a la derecha del segmento . En este caso, podemos agregar a la envolvente y procesar el siguiente punto de la lista dada.
Después de procesar todos los puntos, tenemos la envolvente inferior y empezamos a hallar la envolvente superior de la misma forma. Esta vez, agregamos el punto a la envolvente e iteramos desde el final de los puntos, , hasta el punto inicial . (El punto tampoco tiene por qué quedar en la envolvente convexa y se puede sacar si provoca un giro a la derecha).
Al final, tenemos todos los puntos de la envolvente convexa en orden antihorario. Para hacerlo en orden horario, solo hay que cambiar la condición de de mayor que 0 a menor que 0. En ese caso, se halla primero la envolvente superior y después la inferior.
Este algoritmo tarda en ordenar los puntos y en calcular la envolvente, lo que da una complejidad temporal final de .
Una animación de cómo funciona:
Ejemplo resuelto
Consideremos el mismo ejemplo del algoritmo de Graham Scan.
Hallar la envolvente convexa de los puntos .
Siguiendo los pasos del algoritmo de cadena monótona:
-
Ordenar los puntos dados primero por sus coordenadas y después por sus coordenadas . La lista ordenada sería , , , , ,
-
Hallar la envolvente inferior:
Primero, agregamos los dos primeros puntos a la envolvente. La lista que representa la envolvente ahora contendría estos dos elementos . Empecemos a iterar por el resto de los puntos. Nuestro ahora es . El producto cruz, , es mayor que 0, lo que nos dice que no hay giro a la derecha, y podemos pasar al siguiente punto, . La lista ahora contiene .
El producto cruz es menor que cero, lo que significa que hay un giro a la derecha de a . Por lo tanto, sacamos el punto de la envolvente. La envolvente ahora contiene . Después testeamos el producto cruz , que es positivo, así que agregamos el punto a la envolvente. La envolvente ahora contiene .
Seguimos testeando los puntos restantes. Queremos saber si hay un giro a la derecha de a , y lo hay, así que sacamos el punto . Después testeamos los dos segmentos, y . Hay un giro a la izquierda, así que agregamos el punto y pasamos al siguiente punto. Nuestra lista queda así: .
Después, testeamos los segmentos y . El producto cruz es negativo, así que hay un giro a la derecha. Sacamos de la lista y ahora testeamos los segmentos y . Hay un giro a la izquierda, así que agregamos el punto a la envolvente. La envolvente ahora contiene . Como el punto es el último elemento de la lista, hallamos la envolvente inferior del conjunto de puntos dado.
-
Hallar la envolvente superior:
Primero agregamos el punto al final de la lista y procesamos el punto . El producto cruz para es cero, lo que significa que estos tres puntos son colineales. Por lo tanto, sacamos el punto de la lista. Como no quedan suficientes puntos en la envolvente superior, agregamos el punto a la lista. La envolvente contiene . Notar que los puntos pertenecen a la envolvente inferior y el punto como punto final también pertenece a la envolvente superior.
Después procesamos el siguiente punto . Los segmentos y forman un giro a la izquierda, así que podemos agregar el punto a la envolvente. La lista contiene .
El siguiente punto es . Los segmentos y forman un giro a la izquierda, así que podemos agregar el punto a la envolvente. La lista contiene .
El siguiente punto es . Hay un giro a la derecha de a , así que sacamos el punto de la lista. La lista contiene . Testeamos de nuevo los segmentos y . Hay un giro a la derecha, así que sacamos el punto . La lista contiene . Testeamos otra vez los segmentos y . Hay un giro a la izquierda, así que agregamos el punto a la lista. Llegamos al comienzo de la lista, así que podemos terminar acá. Como ya se había agregado al calcular la envolvente inferior, sacamos el último elemento, es decir el punto agregado , de la lista. Ahora también tenemos calculada la envolvente superior. El resultado final es . Estos son los vértices de nuestra envolvente convexa en orden antihorario.
#include <bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;
vector<pii> points;
vector<pii> hull;
// producto cruz, el área con signo de estos tres puntos
int area(pii O, pii P, pii Q) {
return (P.first - O.first) * (Q.second - O.second) -
(P.second - O.second) * (Q.first - O.first);
}
void monotone_chain() {
// ordenar respecto de las coordenadas x e y
sort(points.begin(), points.end());
// dejar los puntos distintos
points.erase(unique(points.begin(), points.end()), points.end());
int n = points.size();
// 1 o 2 puntos siempre están en la envolvente convexa
if (n < 3) {
hull = points;
return;
}
// envolvente inferior
for (int i = 0; i < n; i++) {
// si con el nuevo punto points[i] se forma un giro a la
// derecha, sacamos el último punto de la envolvente y
// seguimos testeando
while (hull.size() > 1 &&
area(hull[hull.size() - 2], hull.back(), points[i]) <= 0)
hull.pop_back();
// si no, agregar el punto a la envolvente
hull.push_back(points[i]);
}
// envolvente superior, con la misma lógica que la inferior
auto lower_hull_length = hull.size();
for (int i = n - 2; i >= 0; i--) {
// solo podemos sacar un punto si todavía quedan puntos en la
// envolvente superior
while (hull.size() > lower_hull_length &&
area(hull[hull.size() - 2], hull.back(), points[i]) <= 0)
hull.pop_back();
hull.push_back(points[i]);
}
// borrar point[0], que se agregó dos veces
hull.pop_back();
}
int main() {
cin.tie(0)->sync_with_stdio(false);
int n;
cin >> n;
while (n != 0) {
points.assign(n, {});
hull = {};
for (auto &p : points) cin >> p.first >> p.second;
monotone_chain();
cout << hull.size() << "\n";
for (auto &p : hull) cout << p.first << " " << p.second << "\n";
cin >> n;
}
return 0;
}import java.io.*;
import java.util.*;
public class ConvexHull {
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
int N = Integer.parseInt(in.readLine());
while (N > 0) {
// usar un hashset para dejar los puntos distintos
Set<V2> input = new HashSet<V2>(N);
for (int i = 0; i < N; i++) {
StringTokenizer st = new StringTokenizer(in.readLine());
input.add(new V2(Integer.parseInt(st.nextToken()),
Integer.parseInt(st.nextToken())));
}
V2[] points = new V2[input.size()];
input.toArray(points);
var hull = monotoneChain(points);
System.out.println(hull.size());
for (V2 p : hull) { System.out.println(String.format("%d %d", p.X, p.Y)); }
N = Integer.parseInt(in.readLine());
}
in.close();
}
// calcular la envolvente convexa de los puntos dados en orden antihorario
private static List<V2> monotoneChain(V2[] P) {
// para un conjunto con menos de 3 puntos, el conjunto de esos
// puntos ya es la envolvente convexa
if (P.length < 3) { return Arrays.asList(P); }
List<V2> hull = new ArrayList<V2>();
// ordenar los puntos por su valor de x en orden creciente. En
// caso de empate, ordenar por sus valores de y
Arrays.sort(P);
// envolvente inferior
for (int i = 0; i < P.length; i++) {
/*
* si el último punto de hull está a la izquierda del
* punto actual, es decir forma un giro a la derecha,
* entonces podemos sacar el último punto hasta que solo
* queden giros a la izquierda
*/
while (hull.size() > 1 && sign(hull.get(hull.size() - 2),
hull.get(hull.size() - 1), P[i]) <= 0)
hull.remove(hull.size() - 1);
hull.add(P[i]);
}
// envolvente superior
int k = hull.size();
for (int i = P.length - 2; i >= 0; i--) {
while (hull.size() > k && sign(hull.get(hull.size() - 2),
hull.get(hull.size() - 1), P[i]) <= 0) {
hull.remove(hull.size() - 1);
}
hull.add(P[i]);
}
// El primer punto se agregó dos veces, así que lo sacamos
hull.remove(hull.size() - 1);
return hull;
}
/*
* devuelve -1 si el segmento OP está a la izquierda del segmento OQ;
* y 1 en el caso contrario; 0 si O, P y Q son colineales.
*/
private static int sign(V2 O, V2 P, V2 Q) {
int area = area(O, P, Q);
if (area < 0) return -1;
else if (area > 0) return 1;
return 0;
}
// truco del producto cruz, el área con signo de estos tres puntos
private static int area(V2 O, V2 P, V2 Q) { return P.minus(O).cross(Q.minus(O)); }
private static class V2 implements Comparable<V2> {
public int X;
public int Y;
public V2(int x, int y) {
X = x;
Y = y;
}
public V2 minus(V2 other) { return new V2(X - other.X, Y - other.Y); }
// producto "cruz" (wedge) de un vector 2d. si no se está
// familiarizado con esto, conviene investigar primero
public int cross(V2 o) { return X * o.Y - Y * o.X; }
@Override
public int compareTo(ConvexHull.V2 o) {
int r = X - o.X;
if (r == 0) return Y - o.Y;
return r;
}
@Override
public boolean equals(Object obj) {
if (obj == null || !getClass().equals(obj.getClass())) return false;
return ((V2)obj).X == this.X && ((V2)obj).Y == this.Y;
}
@Override
public int hashCode() {
int hash = 17;
hash = hash * 13 + X;
hash = hash * 13 + Y;
return hash;
}
}
}Calibradores rotatorios
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| Kattis | Robert Hood | Fácil | Convex | — |
Solución
| Fuente | Recurso | Notas |
|---|---|---|
| CF | Rotating calipers technique and applications | |
| CF | Rotating calipers - alternate explanation |
Problemas
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| CF | Water Balance | Fácil | Convex | — | |
| Platinum | Balance Beam | Normal | Convex | — | |
| CF | Geometers Anonymous | Normal | Convex, PURS | — | |
| Old Gold | Cow Curling | Normal | Convex | — | |
| Kattis | Fence Orthogonality | Difícil | Convex | — | |
| Balkan OI | ★ 2011 - Timeismoney | Difícil | MST, Convex | Solución | |
| AC | AGC E - Random Pawn | Muy difícil | Convex | — |