When the DP state is a convex piecewise-linear function with integer breakpoints, store it as its minimum value plus two heaps of breakpoints: (max-heap, breakpoints left of the minimum, each one = slope changes by 1) and (min-heap, right of the minimum). Adding , , constants, taking prefix/suffix minima, shifting and sliding-window minima are all or .
struct SlopeTrick {
ll minf = 0, addL = 0, addR = 0; // lazy shifts of the two heaps
priority_queue<ll> L; priority_queue<ll, vector<ll>, greater<>> R;
ll topL() const { return L.empty() ? LLONG_MIN / 4 : L.top() + addL; }
ll topR() const { return R.empty() ? LLONG_MAX / 4 : R.top() + addR; }
void pushL(ll a) { L.push(a - addL); }
void pushR(ll a) { R.push(a - addR); }
ll popL() { ll v = topL(); L.pop(); return v; }
ll popR() { ll v = topR(); R.pop(); return v; }
void addXMinusA(ll a) { minf += max(0LL, topL() - a); pushL(a); pushR(popL()); } // f += max(0, x - a)
void addAMinusX(ll a) { minf += max(0LL, a - topR()); pushR(a); pushL(popR()); } // f += max(0, a - x)
void addAbs(ll a) { addXMinusA(a); addAMinusX(a); } // f += |x - a|
void prefixMin() { R = {}; } // f(x) <- min_{y <= x} f(y)
void suffixMin() { L = {}; } // f(x) <- min_{y >= x} f(y)
void slideMin(ll a, ll b) { addL += a; addR += b; } // f(x) <- min_{x-b <= y <= x-a} f(y), a <= b
void shift(ll a) { addL += a; addR += a; } // f(x) <- f(x - a)
};The minimum is attained on . Evaluating at a point costs (sum the hinge functions), so recover a minimiser from the heaps instead.
Proof: the representation and the update rules
A convex piecewise-linear function with integer slopes is determined by its minimum value and its breakpoints with multiplicity: going right from the flat part, the slope increases by 1 at each element of ; going left, it decreases by 1 at each element of ; convexity is exactly .
Adding raises the slope by 1 at . If , the minimum segment’s left end is unaffected and simply joins (the code pushes to and then moves the maximum of , which is , to ). If , the old flat part now has slope to the right of , so the new flat part is where is the next element of ; the minimum increases by (the hinge’s value at ), and moves from to while joins , which is exactly “push to , move to ”. The other hinge is symmetric, and is the sum of both.
Prefix minimum makes every slope to the right of the minimum 0: drop . Sliding minimum translates the decreasing part right by and the increasing part right by (the flat part widens by ), which is the pair of lazy shifts.
Classic problems
Make the sequence non-decreasing with minimum : . Only survives the prefix minima:
ll minCostNonDecreasing(const vector<ll>& a) { // strictly increasing: use a_i - i
priority_queue<ll> L; ll ans = 0;
for (ll x : a) { L.push(x); if (L.top() > x) { ans += L.top() - x; L.pop(); L.push(x); } }
return ans;
}| Problem | Transition |
|---|---|
| $\min\sum | a_i - b_i |
| same with $ | b_i - b_{i-1} |
| sliding min with | |
| cost $c_i\cdot | x - a_i |
| buy/sell one unit per day for max profit | the heap greedy is the slope trick of the profit function |
| tree version: edges’ lengths adjustable at cost $ | \Delta |
Merging functions: the sum of two convex functions has the union of breakpoints; with only heaps (after prefix minima) merging is heap merging, with small-to-large or with leftist/pairing heaps (Heaps).
Related
- The answer as a function of “number of groups” being convex is the Aliens trick (Aliens); here the function is of a value.
- Non-integer breakpoints are fine as long as comparisons are exact.
- When the function is convex but not piecewise linear with few pieces, keep it in a balanced BST or segment tree over the domain (sum of convex functions, min-plus convolution of convex functions = merge of slope sequences).
Recognising when it applies: the DP value as a function of one parameter must be convex, with transitions limited to adding //constants, taking prefix/suffix minima, or shifting — and you only ever need the final minimum, never to query at an arbitrary point cheaply. If the function isn’t convex, slope trick is simply invalid, not slow.
Several techniques exploit convexity by keeping a different representation of the same kind of object: slope trick keeps the whole convex function in two heaps; CHT and Li Chao tree keep a set of lines (a lower/upper envelope) instead of a function of one variable per DP state; the Aliens trick keeps only one point of a convex sequence indexed by group count, found by binary search rather than maintained directly.