Both maintain sets of lines and answer “best value at ”. A Li Chao tree is a segment tree over where each node keeps the line that wins at its midpoint. Inserts and queries take , with no ordering requirement and no overflow-prone intersection math. A kinetic segment tree keeps one line per array position and answers range max at a time that only increases.

Li Chao tree (dynamic, minimum)

struct Line { ll k, b; ll operator()(ll x) const { return k * x + b; } };
struct LiChao {
    struct Node { Line ln; int l = 0, r = 0; };
    vector<Node> t{{{0, LLONG_MAX}}};               // node 0 unused as a null marker; root = 1
    ll lo, hi; int root = 0;
    LiChao(ll lo, ll hi) : lo(lo), hi(hi) {}
    int insert(int v, ll l, ll r, Line ln) {        // over integer x in [l, r)
        if (!v) { t.push_back({ln}); return sz(t) - 1; }
        ll m = l + (r - l) / 2;
        bool lef = ln(l) < t[v].ln(l), mid = ln(m) < t[v].ln(m);
        if (mid) swap(t[v].ln, ln);
        if (r - l == 1) return v;
        if (lef != mid) { int c = insert(t[v].l, l, m, ln); t[v].l = c; }
        else { int c = insert(t[v].r, m, r, ln); t[v].r = c; }
        return v;
    }
    void add(Line ln) { root = insert(root, lo, hi, ln); }
    ll query(ll x) const {
        ll res = LLONG_MAX, l = lo, r = hi; int v = root;
        while (v) {
            res = min(res, t[v].ln(x));
            ll m = l + (r - l) / 2;
            if (x < m) v = t[v].l, r = m; else v = t[v].r, l = m;
        }
        return res;
    }
};
  • Maximum: insert and negate the answer.
  • Memory: one node per inserted line (at most). Preallocated array version over compressed query points is faster when all are known.
  • Segment insertion (line valid only on ): descend to the canonical nodes and insert there. That’s per insert.
  • Beyond lines: any family where two functions cross at most once on the domain works (for example , , or the cost functions of some DP transitions).
  • Merging Li Chao trees (for DP on trees): merge like segment trees, re-inserting the lines of the smaller structure; the total is amortized because each line only moves down.
  • Persistent Li Chao: path-copy on insert.
Monotone CHT (deque)Dynamic CHT (set)Li Chao
slope ordermust be sortedanyany
query ordersorted for , else binary searchanyany
cost amortized
overflow riskintersection products overflow; need __int128sameonly evaluates lines
codeshortfiddlyshort

DP usage is in Convex Hull Trick.

Kinetic segment tree

Positions hold lines . Time only increases. Operations: replace a line, range max at the current , advance . Each node stores the winning line at the current time and melt = the earliest future time when some winner in its subtree changes. Advancing time only descends into nodes whose melt has passed.

struct KST {
    struct L { ll k, b; ll at(ll x) const { return k * x + b; } };
    int n; ll t = 0; vector<L> best; vector<ll> melt;
    static constexpr ll INF = LLONG_MAX;
    static ll fdiv(ll a, ll d) { return a / d - ((a % d != 0) && ((a < 0) != (d < 0))); }
    void pull(int v) {
        L a = best[2 * v], b = best[2 * v + 1];
        if (a.at(t) < b.at(t) || (a.at(t) == b.at(t) && a.k < b.k)) swap(a, b);   // a wins now
        best[v] = a; melt[v] = min(melt[2 * v], melt[2 * v + 1]);
        if (b.k > a.k) melt[v] = min(melt[v], max(t + 1, fdiv(a.b - b.b, b.k - a.k) + 1));
    }
    void build(int v, int l, int r, const vector<L>& a) {
        if (r - l == 1) { best[v] = a[l]; melt[v] = INF; return; }
        int m = (l + r) / 2; build(2 * v, l, m, a); build(2 * v + 1, m, r, a); pull(v);
    }
    KST(const vector<L>& a) : n(sz(a)), best(4 * sz(a)), melt(4 * sz(a), INF) { build(1, 0, n, a); }
    void set(int v, int l, int r, int i, L x) {
        if (r - l == 1) { best[v] = x; return; }
        int m = (l + r) / 2; i < m ? set(2 * v, l, m, i, x) : set(2 * v + 1, m, r, i, x); pull(v);
    }
    ll query(int v, int l, int r, int ql, int qr) const {
        if (qr <= l || r <= ql) return LLONG_MIN;
        if (ql <= l && r <= qr) return best[v].at(t);
        int m = (l + r) / 2;
        return max(query(2 * v, l, m, ql, qr), query(2 * v + 1, m, r, ql, qr));
    }
    void heat(int v, int l, int r) {
        if (melt[v] > t) return;
        int m = (l + r) / 2; heat(2 * v, l, m); heat(2 * v + 1, m, r); pull(v);
    }
    void advance(ll dt) { t += dt; heat(1, 0, n); }
};

Advancing costs amortized per winner change. Range-restricted time advances (“heaten” only ) need lazy time tags and cost amortized. Typical uses: “add to all in a range, query range max of ” with (the KTT technique), and DP where each candidate’s value grows linearly over time.