Algorithms

The Convex Hull Trick

Minimize a family of lines at a query point — in O(log n), or O(1) amortized

The convex hull trick is a dynamic-programming optimization that evaluates the minimum (or maximum) of a set of linear functions m·x + b at a query point by maintaining the lower (or upper) envelope of the lines. It collapses a DP transition of the form dp[i] = min_j (m_j·x_i + b_j) from O(n) work per state into O(log n) per query — or O(1) amortized when both slopes and queries are monotone — turning a quadratic O(n²) DP into O(n log n) or O(n). The name refers to the lower envelope being the lower boundary of the intersection of half-planes, which is a convex piecewise-linear curve.

  • Query time (sorted envelope)O(log n) binary search
  • Query time (monotone queries)O(1) amortized (pointer sweep)
  • Insert time (monotone slopes)O(1) amortized (stack/deque)
  • Build n linesO(n) sorted / O(n log n) unsorted
  • Li Chao tree alternativeO(log C) any order, online
  • SpaceO(n)
  • DP speedupO(n²) → O(n log n) or O(n)

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

Why the convex hull trick matters

A huge class of one-dimensional dynamic programs has a transition that looks like this:

dp[i] = min over j < i of ( dp[j] + cost(j, i) )

where the cross term cost(j, i) happens to factor into a part that depends on j multiplied by a value read from i, plus leftovers. Written out, each candidate j contributes a value m_j · x_i + b_j — a straight line evaluated at the query coordinate x_i. The slope m_j and intercept b_j depend only on the earlier state j; the query point x_i depends only on the current state i.

Naively you scan every j for every i: O(n) candidates per state, O(n²) total. But for a fixed query x_i, the minimum over a bunch of lines is not some scattered value — it is exactly the height of the lower envelope of those lines at x = x_i. If you maintain that envelope as you go, each minimization becomes a lookup on a convex piecewise-linear curve: O(log n) with binary search, or O(1) amortized if the queries march monotonically. That is the whole trick, and it is the difference between a solution that times out and one that runs in a fraction of a second on n = 10⁶.

How it works, step by step

Picture each candidate as an infinite line y = m·x + b drawn on the plane. Stack all of them together. For any vertical query line x = x_i, the smallest y among all the lines is what the DP wants. The set of points that are the smallest-y for some x forms the lower envelope — a convex, downward-facing chain of line segments. Only the lines that appear on this envelope can ever be the answer; every other line is dominated everywhere and can be discarded.

The three moving parts are:

  • The envelope structure — an ordered list (stack or deque) of the lines that survive, sorted so that as x increases you move along the chain from steeper-negative to shallower slopes (for a lower envelope of a min query, slopes are stored in decreasing order).
  • Insertion — add a new line and pop any previously-stored line that the newcomer has made irrelevant.
  • Query — find, for a given x_i, which segment of the envelope is active, and evaluate that line there.

Insertion: the pop test

Suppose the stack top holds line L1 then L2 (L2 newer), and we want to add L3. Line L2 is now useless — it never wins on any x — precisely when the intersection of L1 and L3 lies at or before the intersection of L1 and L2. In other words, L3 takes over from L1 before L2 ever gets a turn. To avoid floating-point division, this is tested with cross-multiplied integers:

bad(L1, L2, L3) ⟺ (b3 − b1)·(m1 − m2) ≤ (b2 − b1)·(m1 − m3)

If bad is true, pop L2 and re-test with the new top. Because slopes arrive in monotone order, each line is pushed once and popped at most once — O(1) amortized per insertion, O(n) to build the whole envelope.

Query: binary search or pointer sweep

Each consecutive pair of lines on the envelope crosses at a well-defined x. Those crossing points partition the x-axis into intervals, one line ruling each interval. To answer a query x_i:

  • General queries: binary search the crossing points for the interval containing x_i, then evaluate that line — O(log n).
  • Monotone queries: if the x_i values are non-decreasing, keep a pointer that only ever advances forward through the envelope. Across all queries the pointer moves O(n) times total — O(1) amortized each.

The Li Chao tree alternative

The stack/deque version has two hard requirements: slopes must be inserted in monotone order, and (for O(1) queries) query points must be monotone too. Real problems often violate both — insertions and queries interleave online, and slopes come in any order. The Li Chao tree, introduced by competitive programmer Li Chao, removes both constraints.

It is a segment tree built over the domain of possible x values (compressed to O(n) distinct coordinates if the domain is large). Each node covers an x-interval and stores the single line that is dominant at that node's midpoint. Inserting a line walks the tree from the root: at each node it compares the incoming line with the stored line at the midpoint, keeps whichever is lower there, and recurses into the half where the other line might still win. Because a pair of lines crosses at most once, the recursion descends only one side — O(log C) per insertion, where C is the coordinate range. A query walks root-to-leaf collecting the minimum over the stored lines on the path — also O(log C).

Convex hull (stack/deque)Li Chao tree
Insert orderMonotone slopes required (or sort offline)Any order
Query orderAny (binary search) / monotone (pointer)Any
Insert timeO(1) amortized (sorted) / O(log n)O(log C)
Query timeO(log n) / O(1) amortizedO(log C)
Online interleavingHard (deletion is awkward)Natural
Constant factorVery smallLarger (tree traversal)
SpaceO(n)O(C) or O(n) compressed

Rule of thumb: if your slopes and queries are both monotone, use the hull — it is simpler and faster by a constant factor. If either is arbitrary or the operations are interleaved online, reach for the Li Chao tree. For fully dynamic line sets with deletions, the "Kinetic Segment Tree" or an std::set-based dynamic hull (the "LineContainer" idiom) generalizes further at O(log n) per operation.

A worked example: batch scheduling

Consider a classic DP. You must partition n jobs (in order) into contiguous batches. Running a batch costs a fixed setup charge S plus a per-job cost that grows with how late the batch finishes. A common form yields:

dp[i] = min over j < i of ( dp[j] + S + (prefix[i] − prefix[j]) · t[i] )

Expand the product: dp[j] + S + prefix[i]·t[i] − prefix[j]·t[i]. Group the parts by who they depend on. The term prefix[i]·t[i] + S is a constant for a fixed i — pull it outside the min. What remains is:

min over j of ( dp[j] − prefix[j]·t[i] ) = min over j of ( (−prefix[j])·t[i] + dp[j] )

Now it is exactly a line! Candidate j contributes slope m_j = −prefix[j] and intercept b_j = dp[j], queried at x_i = t[i]. Since prefix[j] is non-decreasing, the slopes −prefix[j] arrive in decreasing order — perfect for the monotonic stack. If the query values t[i] are also monotone, the whole DP runs in O(n). Otherwise binary search each query for O(n log n).

How it works in Python (monotonic-stack min hull)

class ConvexHullTrick:
    """Lower envelope for min queries.
    Insert lines with DECREASING slope. Query x may be arbitrary (binary search)."""
    def __init__(self):
        self.m = []   # slopes, stored in decreasing order
        self.b = []   # intercepts

    def _bad(self, l1, l2, l3):
        # True if the middle line l2 is never optimal and can be dropped.
        # Integer cross-product form — no division, no float error.
        # (b3 - b1)*(m1 - m2) <= (b2 - b1)*(m1 - m3)
        return ((self.b[l3] - self.b[l1]) * (self.m[l1] - self.m[l2])
                <= (self.b[l2] - self.b[l1]) * (self.m[l1] - self.m[l3]))

    def add_line(self, m, b):
        # Requires m <= last inserted slope (monotone non-increasing).
        self.m.append(m)
        self.b.append(b)
        while len(self.m) >= 3 and self._bad(len(self.m) - 3,
                                             len(self.m) - 2,
                                             len(self.m) - 1):
            # remove the second-to-last line
            self.m.pop(-2)
            self.b.pop(-2)

    def _eval(self, i, x):
        return self.m[i] * x + self.b[i]

    def query(self, x):
        # Binary search for the line minimizing at x (envelope is convex).
        lo, hi = 0, len(self.m) - 1
        while lo < hi:
            mid = (lo + hi) // 2
            if self._eval(mid, x) > self._eval(mid + 1, x):
                lo = mid + 1
            else:
                hi = mid
        return self._eval(lo, x)


# Batch-scheduling DP driver (slopes -prefix[j] decrease, so stack is valid).
def min_batch_cost(prefix, t, S):
    n = len(prefix)
    dp = [0] * n
    cht = ConvexHullTrick()
    cht.add_line(-prefix[0], dp[0])          # line from j = 0
    for i in range(1, n):
        best = cht.query(t[i])               # min_j (-prefix[j]*t[i] + dp[j])
        dp[i] = best + prefix[i] * t[i] + S
        cht.add_line(-prefix[i], dp[i])      # register i as a future candidate
    return dp[n - 1]

If the query values t[i] were guaranteed non-decreasing, you would replace the binary search in query with a monotone pointer that only advances, dropping the per-query cost to O(1) amortized and the whole DP to O(n).

Common misconceptions and pitfalls

  • Confusing the envelope with the convex hull of points. The name is a slight misnomer: you are not computing the geometric convex hull of a point set (that is Graham scan). You are computing the lower/upper envelope of a set of lines, which is the dual notion — a line y = mx + b maps to the point (m, b), and the envelope corresponds to the hull of those dual points. Same convexity, different object.
  • Using the stack version with out-of-order slopes. If slopes are not monotone, a new line may belong in the middle of the envelope, which the stack cannot insert in O(1). Sort offline first, or use a Li Chao tree. This is the single most common cause of a "wrong answer" on hull-trick problems.
  • Mixing up min and max. A min query needs the lower envelope (slopes decreasing); a max query needs the upper envelope (slopes increasing). Get the direction backwards and you silently return the worst candidate. The cleanest fix is to negate all slopes and intercepts, run the min version, and negate the result.
  • Floating-point intersection tests. Computing crossing points as (b1 − b2) / (m2 − m1) in floating point corrupts the pop decision on tight inputs. Use the integer cross-product bad test, and beware overflow — intermediate products can reach ~10¹⁸, so use 64-bit or 128-bit integers.
  • Assuming monotone queries when they are not. The O(1)-amortized pointer sweep only works if query x values are non-decreasing. If they are not, the pointer can walk backward and the amortized argument collapses — you must binary search instead.
  • Duplicate slopes. Two lines with identical slope never cross; keep only the one with the smaller intercept (for a min hull). Feeding both into the pop test can divide by a zero slope difference if you use the division form.

Where it sits in the DP-optimization family

The convex hull trick is one of a handful of techniques that shave a factor of n off a quadratic DP by exploiting structure in the cost function:

  • Convex hull trick — applies when the cost splits into slope · query + intercept, i.e. the transition is a min/max of lines.
  • Divide-and-conquer optimization — applies when the optimal split point opt[i] is monotone; gives O(n log n) per layer.
  • Knuth's optimization — applies to interval DPs where opt[i][j−1] ≤ opt[i][j] ≤ opt[i+1][j] (the quadrangle inequality); gives O(n²).
  • SMAWK / Monge matrix — a totally-monotone-matrix row-minima technique underlying several of the above.

All of them are refinements of the same broad idea in dynamic programming: avoid re-examining candidates that provable structure guarantees can never be optimal.

Frequently asked questions

What is the time complexity of the convex hull trick?

Building the envelope of n lines costs O(n log n) if the lines arrive in arbitrary slope order (you must sort), or O(n) amortized if they are already sorted by slope and pushed onto a monotonic stack. Each query costs O(log n) via binary search on the envelope, or O(1) amortized if query points are monotone and you sweep a pointer. So a DP with n states and one transition each runs in O(n log n) in general, or O(n) in the fully-monotone case — down from the naive O(n²).

When can I use the convex hull trick to optimize a DP?

When the DP transition has the form dp[i] = min (or max) over j < i of (m_j · x_i + b_j) + (terms depending only on i), where each previous state j contributes a line with slope m_j and intercept b_j, and x_i is a query value read from state i. Each candidate is linear in x_i, so the optimum for any x_i lies on the lower envelope (for min) or upper envelope (for max) of those lines. If m_j and b_j also involve products of i and j, look for a factorization that isolates a per-j slope and a per-j intercept.

What is the difference between the convex hull trick and a Li Chao tree?

The classic convex hull trick keeps an explicit ordered envelope (a stack or deque of lines) and requires either monotone slopes for insertion or offline sorting; it is O(1) amortized per operation in the monotone case. A Li Chao tree is a segment tree over the coordinate domain that stores one dominating line per node; it accepts lines in ANY slope order and queries in any order, both in O(log C) where C is the size of the query domain (or O(log n) with coordinate compression). Use the hull when slopes/queries are monotone and you want minimal constant factors; use Li Chao when they are arbitrary or the insertions and queries are interleaved online.

How does the monotonic-stack version decide when to pop a line?

When adding a new line L3 to a stack whose top two lines are L1 (older) and L2 (newer), L2 is useless if the intersection of L1 and L3 lies at or before the intersection of L1 and L2 — meaning L2 never wins on any x. The standard test bad(L1, L2, L3) compares cross-products of slope and intercept differences to avoid division: (b3 − b1)·(m1 − m2) ≤ (b2 − b1)·(m1 − m3). If it holds, pop L2 and re-test. This keeps the stack strictly increasing in the x-coordinate at which each successive line takes over.

Does the convex hull trick work for maximum instead of minimum?

Yes. For a maximum you maintain the UPPER envelope instead of the lower one. The mechanics are identical up to a sign: either flip the pop comparison, or negate all slopes and intercepts, run the min version, and negate the answer. The only thing that changes is which lines survive — the ones that are highest for some x rather than lowest.

Why does the convex hull trick require sorted slopes for the stack version?

The stack/deque version relies on inserting lines in monotone slope order so that each new line can only be appended at one end, letting popped-off lines never return. If slopes arrive out of order, a new line might need to be inserted in the middle of the envelope, which the stack cannot do in amortized O(1). For arbitrary slope order you must either sort the lines offline first (O(n log n)) or switch to a Li Chao tree, which handles any order online.

What classic problems does the convex hull trick solve?

It optimizes 1D DPs whose transition is a minimum or maximum of linear functions. Canonical examples include batch-scheduling problems that minimize total cost with a fixed setup charge (the DP has cost[i] = min over j of dp[j] + fixed + (prefix[i] − prefix[j]) · something), the 'Acquire' / land-acquisition problems, and many partitioning DPs. It is a member of the same family as Knuth's optimization and the divide-and-conquer DP optimization; the convex hull trick applies specifically when the cost splits into a slope times a query variable plus an intercept.