Rotating Calipers — The >= vs > Bug Warps Bounding Boxes
- Rotating calipers: O(n) scan of antipodal vertex pairs after O(n log n) convex hull. Reduces many O(n²) problems to O(n).
- Each vertex is antipodal to O(1) others as we rotate 360° — the total antipodal pairs is O(n).
- Applications: diameter, minimum width, minimum bounding rectangle, closest pair between hulls.
- Rotating calipers compute geometric properties of convex polygons in O(n) after convex hull construction
- Works by tracking antipodal vertex pairs — pairs with parallel supporting lines
- Each vertex becomes antipodal to at most 2 others as you rotate 360°, so O(n) total pairs
- Antipodal pair tracking reduces problems like diameter, width, and bounding rectangles from O(n²) to O(n)
- Fails on non-convex polygons — must build convex hull first
- The while loop advancing the second pointer is the trickiest part to get right; off-by-one errors are common
Quick Reference: Rotating Calipers Debug Commands
While loop bound miss
print(cross(edge_vec, hull[j] - hull[i])) for each i,jprint('Antipodal pair:', i, j, 'distance:', dist(hull[i], hull[j]))Wrong diameter on rectangle
print('Hull order:', [hull[i] for i in range(n)])cross product check: for each i, cross(hull[i+1]-hull[i], hull[i+2]-hull[i+1]) should all be positiveProduction Incident
Production Debug GuideSymptom → Action guide for the most common implementation bugs
Rotating calipers, introduced by Michael Shamos in 1978, is an O(n) technique for computing geometric properties of convex polygons that would naively require O(n²) comparisons. The key insight: many geometric properties depend only on antipodal vertex pairs — pairs of vertices with parallel supporting lines. As you rotate 360°, each vertex becomes antipodal to at most 2 other vertices, so the total number of antipodal pairs is O(n).
The algorithm works by advancing two pointers around the convex hull, tracking which edge's supporting line defines the current antipodal vertex. The cross product between the current edge and the candidate vertex tells you when the antipodal relationship changes. Get that comparison wrong — use the wrong vertex as the origin — and your diameter comes out wrong. It's one of those algorithms where the intuition is clear but the implementation is finicky.
Here's the thing: you'll likely use a library in production. But when you need to debug why your bounding box is tilted or why the width estimate is off, understanding the antipodal pair loop is what saves your afternoon.
Polygon Diameter — Farthest Pair of Points
The diameter of a convex polygon is the maximum distance between any two vertices. Naively O(n²) — but with rotating calipers, O(n) after the hull is built. The algorithm maintains an index j for the antipodal vertex of edge (i, i+1). As i moves around the hull, j advances only in one direction (mod n), ensuring total O(n) steps.
The cross product between the edge vector and the vector from edge start to candidate vertex tells you if the distance to the candidate line is increasing. When that distance starts decreasing, j has passed the antipodal point for that edge. You then check distances between vertices i and j, and (i+1) and j — one of these will be the maximum for that edge.
from math import dist def polygon_diameter(hull: list[tuple]) -> float: """Maximum distance between any two hull vertices. O(n).""" n = len(hull) if n == 1: return 0 if n == 2: return dist(hull[0], hull[1]) def cross_len(o, a, b): return abs((a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0])) max_dist = 0 j = 1 for i in range(n): # Advance j while cross product increases (antipodal tracking) while cross_len(hull[i], hull[(i+1)%n], hull[(j+1)%n]) > \ cross_len(hull[i], hull[(i+1)%n], hull[j]): j = (j + 1) % n max_dist = max(max_dist, dist(hull[i], hull[j]), dist(hull[(i+1)%n], hull[j])) return max_dist hull = [(0,0),(3,0),(3,4),(0,4)] # 3x4 rectangle — diameter = 5 print(f'Diameter: {polygon_diameter(hull):.3f}') # 5.0 (diagonal)
Minimum Width — The Smallest Enclosing Strip
The minimum width of a convex polygon is the smallest distance between two parallel lines that enclose the entire polygon. This is the minimum over all antipodal pairs of the perpendicular distance from the antipodal vertex to the current edge. For each edge (i,i+1), the width in that direction is the distance from the antipodal vertex j to the line defined by that edge — exactly the cross product length divided by the edge length.
The algorithm is identical to the diameter loop, but instead of tracking maximum distance between vertices, you track the minimum of the perpendicular distances. The cross_len function already gives you twice the area of the triangle (edge, j), which is proportional to the perpendicular distance.
from math import sqrt, dist def polygon_width(hull: list[tuple]) -> float: """Minimum width of convex hull. O(n).""" n = len(hull) if n < 3: return 0.0 def cross_len(o, a, b): return abs((a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0])) def edge_len(i): return dist(hull[i], hull[(i+1)%n]) min_width = float('inf') j = 1 for i in range(n): while cross_len(hull[i], hull[(i+1)%n], hull[(j+1)%n]) > \ cross_len(hull[i], hull[(i+1)%n], hull[j]): j = (j + 1) % n width = cross_len(hull[i], hull[(i+1)%n], hull[j]) / edge_len(i) min_width = min(min_width, width) return min_width hull = [(0,0),(4,0),(4,3),(0,3)] # 4x3 rectangle — width = 3 print(f'Width: {polygon_width(hull):.3f}')
- Diameter: max over vertex-vertex pairs
- Width: min over edge-vertex perpendicular distances
- Width uses edge length to normalise the cross product
- For a rectangle, width = shorter side, diameter = diagonal
Closest Pair Between Two Convex Hulls
Two convex polygons can be separated in O(n + m) using rotating calipers extended to handle two independent hulls. The algorithm maintains two pointers, one on each hull, and finds the pair of points (one per hull) that are closest together. The naive approach is O(nm) — for each vertex of hull A, find the closest vertex on hull B. But with a rotating calipers variant, you can find the minimum distance in linear total time.
The method: start with the vertex of hull A with the smallest x-coordinate and the vertex of hull B with the largest x-coordinate (assuming hulls are disjoint). Then, as you traverse A, you advance B's pointer when the distance starts decreasing. The condition uses the orientation of the edge of A relative to the candidate vertices on B.
This is trickier than the single-hull version because you must handle the case where the closest pair involves a vertex from one hull and an edge of the other. The full solution (Edelsbrunner's algorithm) returns both distance and which points form the closest pair.
from math import dist def closest_pair_convex_hulls(hull_a: list[tuple], hull_b: list[tuple]) -> float: """Minimum distance between two convex polygons. O(n+m).""" # Ensure hulls are sorted CCW n, m = len(hull_a), len(hull_b) if n == 0 or m == 0: return float('inf') # Start with extreme x vertices i = min(range(n), key=lambda idx: hull_a[idx][0]) j = max(range(m), key=lambda idx: hull_b[idx][0]) min_dist = dist(hull_a[i], hull_b[j]) def cross(o, a, b): return (a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0]) # Rotate both pointers, advancing when the other hull's next vertex reduces distance for _ in range(n + m): dist_curr = dist(hull_a[i], hull_b[j]) if dist_curr < min_dist: min_dist = dist_curr # Try advancing on hull A and B next_i = (i+1) % n next_j = (j+1) % m # Determine which hull's next vertex is more promising # Use orientation of edge (i, next_i) relative to candidate from B # Simplified: advance the hull where the next vertex is closer? # (In production, use the more robust criterion based on support lines) if cross(hull_a[i], hull_b[next_j], hull_a[next_i]) >= 0: j = next_j else: i = next_i return min_dist # Example: two squares separated horizontally hull_a = [(0,0),(2,0),(2,2),(0,2)] hull_b = [(5,0),(7,0),(7,2),(5,2)] print(f'Closest distance: {closest_pair_convex_hulls(hull_a, hull_b):.3f}') # 3.000
Minimum Enclosing Rectangle — The Bounding Box Problem
The minimum-area enclosing rectangle (minimum bounding box) of a convex polygon can be found in O(n) using rotating calipers. The rectangle must have one side collinear with a hull edge (the theorem: the optimal rectangle's edge coincides with some hull edge). For each hull edge, you need four antipodal points: the farthest point in the direction perpendicular to the edge (height), and the farthest points in the parallel directions (width extension left and right of the edge's projection).
The algorithm: maintain three antipodal pointers — one for the farthest vertex in the direction of the edge normal (height), and two for the extreme vertices in the direction parallel to the edge (left and right extremes). As the edge rotates, these pointers update. The area of the candidate rectangle = width * height, where width is the projection span and height is the perpendicular distance.
This is used everywhere: packing objects for shipping, collision detection hulls, image processing for bounding oriented objects.
from math import dist, sqrt, acos, pi def min_enclosing_rectangle(hull: list[tuple]): """Return (area, corners) of minimum-area enclosing rectangle.""" n = len(hull) if n < 3: return (0.0, []) # Helper: dot product def dot(a, b): return a[0]*b[0] + a[1]*b[1] def edge_vec(i): return (hull[(i+1)%n][0]-hull[i][0], hull[(i+1)%n][1]-hull[i][1]) def normalize(v): l = sqrt(v[0]*v[0] + v[1]*v[1]) return (v[0]/l, v[1]/l) if l != 0 else (0,0) # Initial pointers: antipodal for height, and extreme left/right along edge j = 1 # farthest in normal direction k = 1 # farthest in -edge direction (left) l = 1 # farthest in +edge direction (right) min_area = float('inf') best_rect = [] for i in range(n): e = edge_vec(i) e_norm = normalize(e) # Normal perpendicular (outward) perp = (-e_norm[1], e_norm[0]) # Advance j while cross product increases while True: next_j = (j+1)%n # Use distance to the line defined by e from hull[i] cur_d = abs((hull[j][0]-hull[i][0])*perp[0] + (hull[j][1]-hull[i][1])*perp[1]) nxt_d = abs((hull[next_j][0]-hull[i][0])*perp[0] + (hull[next_j][1]-hull[i][1])*perp[1]) if nxt_d <= cur_d: break j = next_j # Advance k (left extreme) using dot with -e while True: next_k = (k+1)%n cur_dot = dot((hull[k][0]-hull[i][0], hull[k][1]-hull[i][1]), e) nxt_dot = dot((hull[next_k][0]-hull[i][0], hull[next_k][1]-hull[i][1]), e) if nxt_dot >= cur_dot: # moving away from hull[i] in e direction break k = next_k # Advance l (right extreme) using dot with +e while True: next_l = (l+1)%n cur_dot = dot((hull[l][0]-hull[i][0], hull[l][1]-hull[i][1]), e) nxt_dot = dot((hull[next_l][0]-hull[i][0], hull[next_l][1]-hull[i][1]), e) if nxt_dot <= cur_dot: break l = next_l # Compute rectangle dimensions height = abs((hull[j][0]-hull[i][0])*perp[0] + (hull[j][1]-hull[i][1])*perp[1]) # Width = projection of hull onto e left_dot = dot((hull[k][0]-hull[i][0], hull[k][1]-hull[i][1]), e_norm) right_dot = dot((hull[l][0]-hull[i][0], hull[l][1]-hull[i][1]), e_norm) width = right_dot - left_dot area = width * height if area < min_area: min_area = area # Compute corners (not shown for brevity) return min_area
- Proof by contradiction: if rectangle edge doesn't touch hull, you can shrink until it touches
- If only vertices touch (no edge contact), you can rotate to reduce area
- Thus at least one hull edge must be flush with the rectangle
Implementation Pitfalls and Debugging Pattern
Rotating calipers looks simple on paper but bites you in subtle ways. The most common bugs: wrong while loop condition, off-by-one in pointer advancement, integer overflow in cross product (especially with large coordinates), and ignoring collinear hull points. Also, the algorithm requires a strictly convex hull in counterclockwise order — if your points are convex but in clockwise order, the antipodal relationship reverses.
- Remove collinear points from hull (or handle them explicitly)
- Use 64-bit integers for cross product when coordinates are integers
- Limit the while loop to at most n steps to prevent infinite loops on degenerate hulls
- Test on simple shapes: line, triangle, rectangle, regular hexagon
- Validate against brute-force for small sets before trusting
The antipodal pointer j should never move backward — if it does, your cross product comparison is wrong or the hull is not convex.
| Operation | Naive Approach | Rotating Calipers | Preprocessing Required |
|---|---|---|---|
| Diameter | O(n²) | O(n) | Convex hull O(n log n) |
| Minimum Width | O(n²) | O(n) | Convex hull O(n log n) |
| Closest pair between two hulls | O(nm) | O(n+m) | Convex hulls for each set |
| Minimum Enclosing Rectangle | O(n²) | O(n) | Convex hull O(n log n) |
| Collision detection (separating axis) | O(n²) per frame | O(n) per frame | Convex hull precomputed |
🎯 Key Takeaways
- Rotating calipers: O(n) scan of antipodal vertex pairs after O(n log n) convex hull. Reduces many O(n²) problems to O(n).
- Each vertex is antipodal to O(1) others as we rotate 360° — the total antipodal pairs is O(n).
- Applications: diameter, minimum width, minimum bounding rectangle, closest pair between hulls.
- Always build convex hull first — rotating calipers only works on convex polygons.
- Bug-prone areas: while loop condition (use >=), collinear hull points, integer overflow, hull orientation (must be CCW).
- Test on simple shapes like rectangles and hexagons before deploying to production.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat is an antipodal pair and why does rotating calipers only need to check O(n) pairs?Mid-levelReveal
- QHow would you find the minimum bounding rectangle of a point set using rotating calipers?SeniorReveal
- QWhat is the time complexity of rotating calipers and why can't it be better?JuniorReveal
- QHow would you handle a non-convex polygon with rotating calipers?Mid-levelReveal
Frequently Asked Questions
Does rotating calipers work on non-convex polygons?
No — rotating calipers requires a convex polygon. For non-convex polygons, compute the convex hull first (O(n log n)), then apply rotating calipers to the hull. The hull has at most n vertices, so the total complexity remains O(n log n).
What is the difference between diameter and width?
Diameter is the maximum distance between any two vertices of the convex hull (vertex-vertex). Width is the minimum distance between two parallel supporting lines that enclose the hull (edge-vertex). For a rectangle, diameter = diagonal, width = shorter side.
Why does the antipodal pointer only move forward?
As the edge (i, i+1) rotates, the antipodal vertex for that edge moves monotonically around the hull. Once j passes the antipodal position for edge i, it will never need to go back because the next edge i+1 has an antipodal vertex that is either the same or further ahead. This monotonic property guarantees O(n) total pointer movements.
Can rotating calipers handle integer coordinates?
Yes, but you must be careful with integer overflow in the cross product. For coordinates up to 10^9, the cross product can be up to 2*10^18, which fits in a 64-bit signed integer (9e18). But for larger coordinates, use Python's arbitrary-precision ints or double in other languages. Also, the final distance calculation requires floating point or rational arithmetic.
How do I get the actual points for the diameter pair, not just the distance?
Track the pair (i, j) or (i+1, j) that produced the maximum distance. In the loop, when you update max_dist, also store the corresponding vertex indices. For the other properties like bounding rectangle, you need to store the four corners computed from the edge and the three antipodal pointers.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.