DSA DSA In Depth
0%
lesson convex-hullmonotone-chainrotating-calipers

2D Convex Hull: Graham Scan vs Andrew's Monotone Chain & Rotating Calipers

7 min read Module 6.4: Computational Geometry, Number Theory, Randomized & Approximation Algorithms Updated Oct 23, 2026
📄 Cheat Sheet
Convex Hull Algorithm — Graham Scan and Jarvis March Tutorial
Stable Sort Convex Hull Algorithm — Graham Scan and Jarvis March Tutorial Watch on YouTube ↗

Visually compares polar-angle sorting in Graham Scan against coordinate sorting in Andrew's Monotone Chain, showing how right-turn stack pruning builds the lower and upper hulls.

0:00 / --:--

1. Formal Invariant — Monotone Chain Stack Convexity & Antipodal Pairs

Invariant 1: Andrew's Monotone Chain Stack Property

After sorting distinct points $P_0 < P_1 < \dots < P_{N-1}$ lexicographically by $(x, y)$, both the Lower Hull and Upper Hull stacks satisfy the strict CCW invariant on every consecutive triple $(H_{k-2}, H_{k-1}, H_k)$:

$$\text{cross}(H_{k-2},\; H_{k-1},\; P_i) > 0$$

Whenever $\text{cross}(H_{k-2}, H_{k-1}, P_i) \le 0$ (a right turn or collinear point), $H_{k-1}$ lies inside or on the boundary of $\triangle H_{k-2} H_0 P_i$ and is permanently popped from the hull stack.

Invariant 2: Rotating Calipers Area Monotonicity

On a convex hull $H_0, \dots, H_{M-1}$ in CCW order, for a directed base edge $\vec{e}_i = H_{i+1} - H_i$, the triangle area $\text{cross}(H_i, H_{i+1}, H_j)$ is unimodal as $j$ advances around the hull, allowing the antipodal pointer $j$ to advance monotonically in $O(M)$ total steps.

Andrew Monotone Chain Convex Hull & Sweep-Line Events
O(N log N) · CCW Cross Product

Trace 2D cross-product orientation tests cross(O, A, B) = (Ax-Ox)(By-Oy) - (Ay-Oy)(Bx-Ox) to prune clockwise interior vertices and detect sweep-line segment intersections.

Step 1 / 8
Custom Input:
Timeline

Invariant & State

cross(P0, P1, P2) < 0 (Clockwise / Right Turn): P1 lies strictly inside the lower hull and is popped from the stack.
P0 P1 (Pop!) P2 P3 P4 P5
Inspected Triple
P0 → P1 → P2
Cross Product
cross < 0 (Right Turn)
Stack Action
Pop P1, Push P2
[Step 1] Right turn detected at P1; popped P1 so lower hull edge connects P0 → P2 directly.

2. Andrew's Monotone Chain & Rotating Calipers Pipeline

flowchart LR Input["N Points in 2D"] --> SortLex["Sort Lexicographically by (x, y)
& Deduplicate O(N log N)"] SortLex --> Lower["Sweep Left-to-Right:
Pop while cross <= 0
(Builds Lower Hull)"] SortLex --> Upper["Sweep Right-to-Left:
Pop while cross <= 0
(Builds Upper Hull)"] Lower --> Concat["Concatenate Lower + Upper
Into CCW Convex Hull (Size H)"] Upper --> Concat Concat --> Calipers["Rotating Calipers Two-Pointer:
Advance j while cross(e_i, H_{j+1}-H_j) > 0
O(H) Diameter"]

3. Why Andrew's Monotone Chain Beats Graham Scan in Practice

  • Faster Sorting Key: Comparing two integers a.x < b.x || (a.x == b.x && a.y < b.y) takes 1–2 CPU cycles and branches predictably, whereas Graham Scan's polar comparator requires a 64-bit cross product + distance tie-breaker on every std::sort comparison ($O(N \log N)$ multiplications).
  • Clean Collinear Handling: In Graham Scan, collinear points along the final closing ray back to the pivot require reversing the suffix of the sorted array if you want all boundary points. In Andrew's Monotone Chain, simply changing < 0 vs <= 0 in the while loop toggles between keeping or discarding collinear boundary vertices!

4. Hardware & Cache Reality

Amortized $O(N)$ Stack Scans & SIMD Sorting

After the initial $O(N \log N)$ sort, the lower and upper hull construction loops stream sequentially through pts[0..N-1] and push/pop at the tail of a single contiguous std::vector<Point>. Each point is pushed twice and popped at most twice ($\le 4N$ stack ops total), executing in sub-millisecond L1 cache time.

5. Complexity Matrix

Algorithm Time Complexity Space Complexity Best Suited When
Andrew's Monotone Chain O(N log N) O(N) General 2D Convex Hull ($O(N)$ if pre-sorted by $x$)
Graham Scan O(N log N) O(N) Radially sorted point sets
Jarvis March (Gift Wrapping) O(N * H) O(H) Output-sensitive when hull size $H \ll \log N$
Chan's Algorithm O(N log H) O(N) Optimal output-sensitive 2D/3D Convex Hull
Rotating Calipers (Diameter / Width) O(H) after hull O(1) Farthest pair of points, minimum bounding box

6. Production-Grade C++20 Implementation (Monotone Chain + Rotating Calipers Diameter)

#include <vector>
#include <cstdint>
#include <algorithm>

struct Pt {
    int64_t x = 0, y = 0;
    bool operator<(const Pt& o) const { return x != o.x ? x < o.x : y < o.y; }
    bool operator==(const Pt& o) const { return x == o.x && y == o.y; }
    Pt operator-(const Pt& o) const { return {x - o.x, y - o.y}; }
};

inline int64_t cross(const Pt& a, const Pt& b, const Pt& c) {
    return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
}

inline int64_t dist_sq(const Pt& a, const Pt& b) {
    int64_t dx = a.x - b.x, dy = a.y - b.y;
    return dx * dx + dy * dy;
}

// Returns strictly convex hull in CCW order (no 3 collinear points)
std::vector<Pt> monotone_chain_hull(std::vector<Pt> pts) {
    std::sort(pts.begin(), pts.end());
    pts.erase(std::unique(pts.begin(), pts.end()), pts.end());
    const int n = static_cast<int>(pts.size());
    if (n <= 1) return pts;

    std::vector<Pt> hull;
    hull.reserve(2 * n);

    // Lower hull
    for (int i = 0; i < n; ++i) {
        while (hull.size() >= 2 &&
               cross(hull[hull.size() - 2], hull.back(), pts[i]) <= 0) {
            hull.pop_back();
        }
        hull.push_back(pts[i]);
    }
    // Upper hull
    const std::size_t lower_sz = hull.size();
    for (int i = n - 2; i >= 0; --i) {
        while (hull.size() > lower_sz &&
               cross(hull[hull.size() - 2], hull.back(), pts[i]) <= 0) {
            hull.pop_back();
        }
        hull.push_back(pts[i]);
    }
    hull.pop_back(); // Last point duplicates pts[0]
    return hull;
}

// Rotating Calipers: Returns maximum squared Euclidean distance (Diameter^2) in O(H)
int64_t rotating_calipers_diameter_sq(const std::vector<Pt>& hull) {
    const int h = static_cast<int>(hull.size());
    if (h <= 1) return 0;
    if (h == 2) return dist_sq(hull[0], hull[1]);

    int64_t max_d2 = 0;
    int j = 1;
    for (int i = 0; i < h; ++i) {
        int ni = (i + 1) % h;
        // Advance antipodal vertex j while triangle area increases
        while (cross(hull[i], hull[ni], hull[(j + 1) % h]) >
               cross(hull[i], hull[ni], hull[j])) {
            j = (j + 1) % h;
        }
        max_d2 = std::max({max_d2, dist_sq(hull[i], hull[j]), dist_sq(hull[ni], hull[j])});
    }
    return max_d2;
}

Production Pitfall — Advancing Rotating Calipers by Euclidean Distance Instead of Cross-Product Area

When implementing Rotating Calipers for the farthest pair of points, a classic mistake is advancing pointer j while dist_sq(hull[i], hull[j+1]) > dist_sq(hull[i], hull[j]). Squared distance from a single vertex hull[i] is NOT necessarily unimodal around an arbitrary convex polygon, and two-pointer advancing on dist_sq fails on elongated polygons! Always advance j by comparing perpendicular height (triangle area) cross(hull[i], hull[i+1], hull[j]) against the base edge $\overline{H_i H_{i+1}}$, which is strictly unimodal on any convex hull.

Check yourself Question 1 of 4
Concept Check Core Invariant Check recall

How does Andrew's Monotone Chain algorithm avoid the polar-angle sorting step required by Graham Scan?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

In Andrew's Monotone Chain, suppose you want to retain **all** points that lie on the boundary of the convex hull (including collinear intermediate points on hull edges) by changing the pop condition from `cross(...) <= 0` to `cross(...) < 0`. What edge case arises when all $N$ input points are collinear on a single line, and how do you handle it?

Key Takeaways

Andrew's Monotone Chain sorts points lexicographically by (x, y) in O(N log N) and constructs the lower and upper convex hulls in O(N) stack operations without polar angle arithmetic.
Using cross(...) <= 0 pops collinear boundary points to yield a minimal vertex hull, whereas cross(...) < 0 retains all boundary points (requiring deduplication of identical points first).
Rotating Calipers finds the diameter (farthest pair of points) of a convex hull in O(H) time by sweeping antipodal pairs around the hull.
Always advance the Rotating Calipers pointer using triangle area cross(H_i, H_{i+1}, H_j) rather than point-to-point distance, because perpendicular distance to an edge is guaranteed unimodal on a convex hull.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue