For transitions , each earlier state is a line and the query is the lower envelope at . Recognise it by expanding the cost: whatever multiplies a function of by a function of is slope × query point. Example: gives slope , intercept , point , plus .

Slopes insertedQueriesStructureCost
monotonemonotonedeque, pop from the front
monotonearbitrarydeque/stack + binary search
arbitraryarbitraryLi Chao tree (Li Chao) or LineContainer /
arbitrary, offlinearbitraryCDQ on index + monotone hull inside
lines with a validity range of arbitraryLi Chao with segment insertion, or segment tree over time
lines with deletions, offlinearbitrarysegment tree over time + Li Chao with rollback
on a tree (lines from ancestors)arbitrarypersistent Li Chao, or monotone hull with rollback during DFS

Monotone hull (min, slopes decreasing, queries increasing)

struct MonoCHT {
    deque<pair<ll, ll>> h;                                  // (slope, intercept), slopes strictly decreasing
    static bool bad(pair<ll, ll> a, pair<ll, ll> b, pair<ll, ll> c) {   // b never strictly below min(a, c)
        return (__int128)(c.second - a.second) * (a.first - b.first) <= (__int128)(b.second - a.second) * (a.first - c.first);
    }
    void add(ll m, ll c) {
        if (sz(h) && h.back().first == m) { if (h.back().second <= c) return; h.pop_back(); }
        while (sz(h) >= 2 && bad(h[sz(h) - 2], h.back(), {m, c})) h.pop_back();
        h.push_back({m, c});
    }
    ll at(int i, ll x) const { return h[i].first * x + h[i].second; }
    ll query(ll x) { while (sz(h) >= 2 && at(0, x) >= at(1, x)) h.pop_front(); return at(0, x); }
    ll queryAny(ll x) const {                               // arbitrary x: binary search on the envelope
        int lo = 0, hi = sz(h) - 1;
        while (lo < hi) { int mid = (lo + hi) / 2; if (at(mid, x) >= at(mid + 1, x)) lo = mid + 1; else hi = mid; }
        return at(lo, x);
    }
};

For max, negate slopes and intercepts (or flip the comparisons). If slopes arrive increasing, mirror .

Fully dynamic: line container

A multiset of lines sorted by slope, each storing the where it stops being optimal; inserting removes dominated neighbours. Query by binary searching the breakpoints. (Max version; for min insert and negate.)

struct Line { mutable ll k, m, p; bool operator<(const Line& o) const { return k < o.k; } bool operator<(ll x) const { return p < x; } };
struct LineContainer : multiset<Line, less<>> {
    static const ll INF = LLONG_MAX;
    static ll fdiv(ll a, ll b) { return a / b - ((a ^ b) < 0 && a % b); }
    bool isect(iterator x, iterator y) {                    // set x->p; true if y is now useless
        if (y == end()) return x->p = INF, false;
        if (x->k == y->k) x->p = x->m > y->m ? INF : -INF;
        else x->p = fdiv(y->m - x->m, x->k - y->k);
        return x->p >= y->p;
    }
    void add(ll k, ll m) {
        auto z = insert({k, m, 0}), y = z++, x = y;
        while (isect(y, z)) z = erase(z);
        if (x != begin() && isect(--x, y)) isect(x, y = erase(y));
        while ((y = x) != begin() && (--x)->p >= y->p) isect(x, erase(y));
    }
    ll query(ll x) const { auto l = *lower_bound(x); return l.k * x + l.m; }
};

Variations

  • Two-parameter costs with an extra constraint : segment tree of hulls (each node a static hull of its lines), .
  • Divide and conquer (CDQ) on the index when slopes or queries aren’t monotone but you want deque speed: solve the left half, build a hull of its lines sorted by slope, answer the right half’s queries sorted by , recurse.
  • “Add a line, query a point” with per-element growth (values grow linearly with time): kinetic segment tree (KST).
  • Functions that cross at most once (not just lines): Li Chao works unchanged.
  • When the transition is with Monge but not linear, use the monotone-queue / divide-and-conquer machinery instead (Monotone Optimizations).