Optimise a DP of the form

from to or . Each earlier state contributes a line; the answer at is the lower envelope of those lines evaluated at .

The geometry

A set of lines has a lower envelope that is a convex piecewise-linear function. Lines that are never minimal anywhere can be discarded permanently. Maintaining that envelope is the whole technique.

A line is unnecessary if, for lines in slope order, lies above the intersection of and :

bool bad(Line a, Line b, Line c) {              // is b unnecessary?
    // intersection of a,c is left of or at the intersection of a,b
    return (long double)(c.c - a.c) * (a.m - b.m) <= (long double)(b.c - a.c) * (a.m - c.m);
}

Monotone case —

When slopes are added in monotone order and queries arrive in monotone order, a deque plus a moving pointer gives amortized per operation.

struct CHT {
    vector<long long> M, C;
    int ptr = 0;
    void add(long long m, long long c) {         // slopes added in decreasing order (for min)
        while (M.size() >= 2 && bad(M.size() - 2, M.size() - 1, m, c)) { M.pop_back(); C.pop_back(); }
        M.push_back(m); C.push_back(c);
    }
    long long query(long long x) {               // x non-decreasing
        ptr = min<int>(ptr, M.size() - 1);
        while (ptr + 1 < (int)M.size() && M[ptr+1]*x + C[ptr+1] <= M[ptr]*x + C[ptr]) ptr++;
        return M[ptr] * x + C[ptr];
    }
};

If slopes are monotone but queries are not, replace the pointer with a binary search per query.

Arbitrary order — Li Chao tree

When neither slopes nor queries are monotone, use a Li Chao tree: a segment tree over the range where each node stores the line that is best at its midpoint. Insert and query are both , and the code is far shorter than a fully dynamic hull.

struct LiChao {
    struct Line { long long m, c; long long operator()(long long x) const { return m*x + c; } };
    vector<Line> t;                                // sized 4 * range
    void insert(int node, int l, int r, Line nw) {
        int m = (l + r) / 2;
        bool lef = nw(l) < t[node](l), mid = nw(m) < t[node](m);
        if (mid) swap(t[node], nw);
        if (l == r) return;
        if (lef != mid) insert(2*node, l, m, nw);
        else            insert(2*node+1, m+1, r, nw);
    }
    long long query(int node, int l, int r, long long x) {
        if (l == r) return t[node](x);
        int m = (l + r) / 2;
        return min(t[node](x), x <= m ? query(2*node, l, m, x) : query(2*node+1, m+1, r, x));
    }
};

Li Chao also handles arbitrary functions that pairwise cross at most once — not just lines.

Recognising the pattern

The trick applies whenever the transition factorises into a product of something depending on and something depending on :

Example. expands to

Choosing an implementation

SlopesQueriesStructureTime
monotonemonotonedeque + pointer
monotonearbitraryvector + binary search
arbitraryarbitraryLi Chao tree
arbitraryarbitrarydynamic hull (multiset)
lines added and removedarbitraryLi Chao + offline segment tree on time
divide and conquer over the arrayCHT inside CDQ

Overflow and precision

The bad test multiplies differences of slopes and intercepts — with values up to and this overflows long long. Use __int128 for the comparison, or long double if a small precision risk is acceptable. Li Chao avoids the issue entirely by only ever comparing line values, which is a good reason to prefer it.

See also: Li Chao Tree · D&C DP · Slope Trick