Appearance
Slope Trick
Quick Revisit
- USE WHEN: DP cost function is convex piecewise-linear and transitions involve adding absolute values or taking prefix-min/max
- INVARIANT: Max-heap stores left-side breakpoints (slopes ≤ 0), min-heap stores right-side breakpoints (slopes ≥ 0); base value tracks the minimum
- TIME: O(n log n) | SPACE: O(n)
- CLASSIC BUG: Forgetting to update the base (minimum) value when a breakpoint crosses from the left heap to the right heap or vice versa
- PRACTICE: 04-ladder-dp
AL-34 | Optimize convex piecewise-linear DP transitions from
Difficulty: [Advanced]Prerequisites: DP -- 1D Introduction, Priority Queue & HeapsCF Rating Range: 1900 -- 2100
Contents
- Intuition
- STL / API Quick Reference
- Implementation (Contest-Ready)
- Complexity Analysis
- Common Patterns
- Contest Cheat Sheet
- Gotchas & Debugging
- Self-Test
- Practice Problems
- Further Reading
Intuition
2.1 The Pain
You have a sequence of sensor readings that drifts up and down, and you need to smooth it into a non-decreasing signal while minimizing the total adjustment. The natural DP tracks every possible output value at each position -- and that table is enormous.
Problem: Given an array
The standard DP: let
The value
Concrete example (
| 1 | 2 | 2 | 2+0=2 | 2 | 2+3=5 |
| 2 | 1 | 1 | 1+1=2 | 2 | 2+2=4 |
| 3 | 0 | 0 | 0+2=2 | 2 | 2+1=3 |
| 4 | 1 | 0 | 0+3=3 | 2 | 2+0=2 |
Answer:
Time complexity:
2.2 The Key Insight
Look at the shape of
: a V-shape centered at . Slopes are then . - After prefix-min (
): slopes clamp to at most on the right -- the function flattens. - Adding
: another V-shape is added -- slopes shift by at the new breakpoint.
Each
A max-heap stores the breakpoints of the "left side" (slopes
Total:
2.3 Visual Walkthrough
Array:
Step 0: Process
cost
3 | . Slopes: ..., -1, +1, ...
2 | . . Breakpoints (where slope increases by 1):
1 |. . Left heap (L): {3}
0 +--*--.---> v Right heap (R): {3}
0 1 2 3 4 5 min_cost = 0The function has slope
Step 1: Process
(a) Add
cost Before prefix-min:
4 |. L = {3, 1}, R = {1, 3}
3 | . . Slopes: -2, 0, +2
2 | . . (two V-shapes stacked)
1 | . . .
0 +----*.*------> v
0 1 2 3 4 5(b) Prefix-min (
Actually, the operation is: the right slopes are capped at
For our representation: push
After adding
Prefix-min: pop everything from
Wait -- we should track this more carefully. Let me use the standard slope trick formulation.
After Step 1 (add
cost After prefix-min:
2 |. L = {1, 1}, R = {} (empty)
1 | . Slopes: ..., -2, -1, 0, 0, 0, ...
0 +--*-*-----------> v The function is flat from v=1 onward.
0 1 2 3 4 5 min_cost = 2Step 2: Process
cost
5 |.
4 | . L = {4, 1, 1}, R = {4}
3 | . . Slopes: ..., -3, -2, -1, +1, ...
2 +---*-*-----*----> v min_cost = 2
0 1 2 3 4 5Answer:
2.4 The Invariant
At every step, the DP cost function
is convex and piecewise linear with integer slopes. The max-heap stores breakpoints on the left (each breakpoint increments the slope by going left-to-right, i.e., the slope is one less to its left). The min-heap stores breakpoints on the right. Together with the tracked minimum value, these fully determine .
More precisely, if
This is convex, piecewise linear, and fully determined by the heaps + the minimum.
2.5 When to Reach for This
Trigger phrases:
- "Minimize total cost to make a sequence non-decreasing (or non-increasing)."
- "Cost function uses absolute values
." - "The DP over values is convex and piecewise linear."
- "Value range is huge (
) but only elements."
Does NOT apply when:
- The cost is quadratic (
) -- different technique (isotonic regression). - The DP function is not convex (e.g., concave or arbitrary shape).
- You need the actual optimal
array, not just the cost (slope trick gives cost; recovering needs extra bookkeeping).
What the Code Won't Teach You
Reading the implementation, you see two heaps and some push/pop operations. Here's what that code means but doesn't say:
The core idea: represent the function by its slope changes, not its values. Instead of storing
Why two priority queues (heaps)? Because the function is convex, its slopes are non-decreasing:
- Left breakpoints (L, max-heap): where the slope increments on the left side (slopes <= 0). A max-heap because we need fast access to the rightmost left breakpoint (the edge of the minimum region).
- Right breakpoints (R, min-heap): where the slope increments on the right side (slopes >= 0). A min-heap because we need the leftmost right breakpoint.
── Diagram A: Piecewise Linear Convex Function & Heap Contents ──
f(x) with breakpoints at x = 1, 3, 5, 7:
cost
6 |\. ./
5 | \. ./
4 | \. ./ Slopes (left to right):
3 | \. ___ ./ -3, -2, -1, 0, +1, +2
2 | \. / \ ./
1 | \. ./ \. ./ Breakpoints where slope
0 +------*--*--------*--*-----> x changes by +1:
1 3 5 7
L (max-heap): {3, 1}
◄─── left ──► ◄─ right─► (slope <= 0 side)
slopes <= 0 flat slopes >= 0 R (min-heap): {5, 7}
(slope >= 0 side)
min_val = 0When does slope trick apply? Look for these ingredients: (1) cost functions involve
The "add min_val.
── Diagram B: The "add |x - a|" Operation (a = 4) ──
BEFORE: f(x) |x - 4|:
cost cost
3 |\. flat 2 |\. ./
2 | \. 1 | \. . /
1 | \. 0 +---\-*-/----> x
0 +---*--*-----------> x 2 4 6
1 3
L={3,1} R={} min=2 V-shape at x = 4
AFTER: f(x) + |x - 4|
cost
5 |\.
4 | \. ./
3 | \. . / L = {3, 1} -> push 4 -> L = {4, 3, 1}
2 +---*--*-----*./ R = {} -> push 4 -> R = {4}
1 3 4 L.top()=4, R.top()=4 -> 4 <= 4 OK
No swap needed. min_val stays 2.
Slopes: -2 -1 0 +1
New breakpoint at x=4 added to both heaps. ── Diagram C: Two-Heap Representation ──
L (max-heap) R (min-heap)
┌────────────┐ ┌────────────┐
│ top -> 3 │ │ top -> 5 │
│ / \ │ │ / \ │
│ 1 2 │ │ 7 6 │
└────────────┘ └────────────┘
│ │
Left breakpoints Right breakpoints
(slope changes on (slope changes on
left of minimum) right of minimum)
◄── all of L ──► <= ◄── all of R ──►
Invariant: max(L) <= min(R)
Together with min_val, this FULLY describes f(x):
f(x) = min_val + Σ max(0, lₖ - x) + Σ max(0, x - rₖ)
lₖ ∈ L rₖ ∈ R
┌─────────────────────────────────────────────────┐
│ With lazy offsets (L_offset, R_offset): │
│ actual value = stored value + offset │
│ push(x): heap.push(x - offset) │
│ top(): heap.top() + offset │
│ "shift all by δ": offset += δ <- O(1)! │
└─────────────────────────────────────────────────┘3. STL / API Quick Reference
| Function / Class | Header | What it does | Time |
|---|---|---|---|
priority_queue<T> | <queue> | Max-heap for left breakpoints | |
priority_queue<T, vector<T>, greater<T>> | <queue> | Min-heap for right breakpoints | |
top() | <queue> | Peek at the largest (max-heap) or smallest (min-heap) breakpoint | |
push(x) | <queue> | Insert a breakpoint | |
pop() | <queue> | Remove top breakpoint |
Lazy offset trick: Instead of shifting all breakpoints in a heap (e.g., "add
Implementation (Contest-Ready)
Version A: Make Non-Decreasing (Minimum )
Minimal contest template:
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<ll> a(n);
for (auto& x : a) cin >> x;
// L = max-heap (left breakpoints), R = min-heap (right breakpoints)
priority_queue<ll> L;
priority_queue<ll, vector<ll>, greater<ll>> R;
ll min_cost = 0;
for (int i = 0; i < n; i++) {
// Add |v - a[i]|: push a[i] to both heaps
L.push(a[i]);
R.push(a[i]);
// Restore convexity: if left top > right top, swap them
if (L.top() > R.top()) {
ll lt = L.top(), rt = R.top();
L.pop(); R.pop();
// The swap costs (lt - rt) added to the minimum
min_cost += lt - rt;
L.push(rt);
R.push(lt);
}
// Prefix-min: discard all right breakpoints
// (makes function flat on the right = non-decreasing constraint)
while (!R.empty()) R.pop();
}
cout << min_cost << "\n";
return 0;
}Explained version:
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<ll> a(n);
for (auto& x : a) cin >> x;
// We represent f_i(v) = cost of making a[0..i] non-decreasing
// with b[i] = v, using its breakpoints.
//
// L (max-heap): breakpoints on the left of the minimum.
// Each breakpoint p means "slope increases by +1 at v = p"
// going left to right (so slope is more negative to the left).
// R (min-heap): breakpoints on the right of the minimum.
// Each breakpoint p means "slope increases by +1 at v = p".
// min_cost: the minimum value of f_i.
//
// Together: f_i(v) = min_cost
// + sum of max(0, l_k - v) for l_k in L
// + sum of max(0, v - r_k) for r_k in R
priority_queue<ll> L; // max-heap
priority_queue<ll, vector<ll>, greater<ll>> R; // min-heap
ll min_cost = 0;
for (int i = 0; i < n; i++) {
// --- Transition: add |v - a[i]| to f ---
// |v - a[i]| adds a breakpoint at a[i] on both sides:
// slope decreases by 1 just left of a[i] (push to L)
// slope increases by 1 just right of a[i] (push to R)
L.push(a[i]);
R.push(a[i]);
// The heaps must satisfy: all of L <= all of R.
// If L.top() > R.top(), the function has a "dip" that
// violates convexity with the intended partition.
// Fix: swap the offending tops. The cost of the swap
// (the overlap) gets added to min_cost.
if (L.top() > R.top()) {
ll lt = L.top(), rt = R.top();
L.pop(); R.pop();
min_cost += lt - rt;
L.push(rt);
R.push(lt);
}
// --- Prefix-min: enforce non-decreasing constraint ---
// min_{u <= v} f(u) flattens the function for v beyond
// the minimizer. All right-side breakpoints are removed.
while (!R.empty()) R.pop();
}
cout << min_cost << "\n";
return 0;
}Version B: General Slope Trick Template (with Lazy Offsets)
For problems that combine operations (shift, add absolute value, prefix-min, suffix-min), use lazy offsets on both heaps to avoid touching every element.
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct SlopeTrick {
priority_queue<ll> L; // max-heap
priority_queue<ll, vector<ll>, greater<ll>> R; // min-heap
ll min_val = 0;
ll L_offset = 0, R_offset = 0;
void push_L(ll x) { L.push(x - L_offset); }
void push_R(ll x) { R.push(x - R_offset); }
ll top_L() { return L.empty() ? -1e18 : L.top() + L_offset; }
ll top_R() { return R.empty() ? 1e18 : R.top() + R_offset; }
ll pop_L() { ll v = top_L(); L.pop(); return v; }
ll pop_R() { ll v = top_R(); R.pop(); return v; }
// Add |v - a| to f(v)
void add_abs(ll a) {
push_L(a);
push_R(a);
if (top_L() > top_R()) {
ll l = pop_L(), r = pop_R();
min_val += l - r;
push_L(r);
push_R(l);
}
}
// f(v) <- min_{u <= v} f(u) (prefix-min: flatten right side)
void prefix_min() {
while (!R.empty()) R.pop();
}
// f(v) <- min_{u >= v} f(u) (suffix-min: flatten left side)
void suffix_min() {
while (!L.empty()) L.pop();
}
// f(v) <- f(v - c) (shift function right by c)
void shift_right(ll c) {
L_offset += c;
R_offset += c;
}
// f(v) <- min_{v-b <= u <= v+a} f(u)
// Sliding window minimum: expand left by b, right by a.
void sliding_window(ll a, ll b) {
L_offset -= b;
R_offset += a;
}
ll get_min() { return min_val; }
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<ll> a(n);
for (auto& x : a) cin >> x;
SlopeTrick st;
for (int i = 0; i < n; i++) {
st.add_abs(a[i]);
st.prefix_min();
}
cout << st.get_min() << "\n";
return 0;
}Complexity Analysis
| Operation | Time | Space |
|---|---|---|
add_abs(a) | ||
prefix_min() / suffix_min() | frees memory | |
shift_right(c) | ||
sliding_window(a, b) | ||
| Full "make non-decreasing" |
Why
Space: At most
Common Patterns
Pattern 1: Make Non-Decreasing ( )
The canonical problem. Process left to right: add_abs(a[i]) then prefix_min().
SlopeTrick st;
for (int i = 0; i < n; i++) {
st.add_abs(a[i]);
st.prefix_min();
}
cout << st.get_min();Examples: CF 713C -- Sonya and Problem Wihtout a Legend
Pattern 2: Make Non-Increasing
Mirror of Pattern 1. Process left to right: add_abs(a[i]) then suffix_min(). Alternatively, reverse the array and apply Pattern 1.
SlopeTrick st;
for (int i = 0; i < n; i++) {
st.add_abs(a[i]);
st.suffix_min();
}
cout << st.get_min();Pattern 3: Make Strictly Increasing
Reduce to non-decreasing: replace
for (int i = 0; i < n; i++) a[i] -= i;
// Now apply Pattern 1Examples: CF 713C (variant asking for strictly increasing)
Pattern 4: Bounded Modification (Sliding Window Min)
If prefix_min() with sliding_window(d, d):
SlopeTrick st;
for (int i = 0; i < n; i++) {
st.add_abs(a[i]);
st.sliding_window(d, d); // min over [v-d, v+d]
}This expands the left breakpoints left by
Examples: CF 1534F -- Fall Guys
Pattern 5: Weighted Absolute Value ( )
If each position has weight
for (int k = 0; k < w[i]; k++) {
st.push_L(a[i]);
st.push_R(a[i]);
}
// Then do the swap-and-fix stepPattern 6: Combining Two Slope Trick Functions
If
Merge the smaller heap into the larger (small-to-large merging) for
Examples: CF 1534F2 -- Fall Guys (Hard)
Contest Cheat Sheet
+-----------------------------------------------------------------+
| SLOPE TRICK -- CHEAT SHEET |
+-----------------------------------------------------------------+
| WHEN TO USE: |
| "Minimize sum |a_i - b_i| subject to monotonicity on b" |
| DP over values is convex piecewise-linear with huge V range |
+-----------------------------------------------------------------+
| REPRESENTATION: |
| L = max-heap of left breakpoints (slope changes on left) |
| R = min-heap of right breakpoints (slope changes on right) |
| min_val = minimum value of the function |
| Invariant: all of L <= all of R |
+-----------------------------------------------------------------+
| OPERATIONS: TIME: |
| add_abs(a): O(log n) |
| push a to L and R |
| if L.top > R.top: swap tops, add diff to min_val |
| prefix_min(): O(k) |
| clear R (flatten right side) |
| suffix_min(): O(k) |
| clear L (flatten left side) |
| shift_right(c): O(1) |
| L_offset += c; R_offset += c |
| sliding_window(a,b): O(1) |
| L_offset -= b; R_offset += a |
+-----------------------------------------------------------------+
| REDUCTIONS: |
| Strictly increasing: a[i] -= i, then non-decreasing |
| Non-increasing: suffix_min instead of prefix_min |
| Weighted cost w*|.|: push a[i] w times to each heap |
+-----------------------------------------------------------------+
| ANSWER: st.get_min() after processing all elements |
+-----------------------------------------------------------------+Gotchas & Debugging
Forgetting the swap after
add_abs. After pushingto both heaps, you must check if and swap. Without this, the invariant breaks and the function is no longer convex. Symptom: wrong answer on almost every test. Prefix-min clears
, not . A common mix-up: prefix_minmakes the function flat to the right (non-decreasing constraint), so right breakpoints are discarded. For non-increasing, clearinstead. Lazy offset accounting. When using
L_offset/R_offset, every push must subtract the offset (push(x - offset)) and every top/pop must add it back. Off-by-one in the offset direction causes subtle WA.Strictly increasing needs the
trick. If you forget to subtract , you solve the non-decreasing version instead. If the problem says "strictly", always transform first. long longeverywhere. Costs can accumulate to. Use llformin_val, offsets, and heap elements.Recovering the optimal array
. Slope trick gives only the cost, not the actual values. To recover , process in reverse: after computing the answer, pop from to determine each from right to left. This is error-prone; practice it separately. Empty heap access. Before calling
L.top()orR.top(), check that the heap is non-empty. In the first iterationand each have exactly one element, so this is fine after add_abs, but be careful if your logic differs.Merging heaps (tree DP). When combining slope trick functions from children on a tree, merge the smaller heap into the larger. Using
std::priority_queuemeans popping all of the smaller heap -- there's no built-in merge. Consider__gnu_pbdsor a custom mergeable heap for better constants.Stress testing. For small
( ) and small ( ), compare against the DP table. Generate random arrays and check that both approaches agree. Forgetting to update
min_valduring the swap. WhenL.top() > R.top()after pushing, you pop both, swap them, and add the overlapL.top() - R.top()tomin_val. The tracked minimum is not automatic from the heaps alone -- it's the third piece of state, and the swap is the only place it moves. Miss this update and every test fails by the accumulated overlap. Write the formulamin_val += l - r;as a comment next to the swap so you don't lose it during refactors.
Mental Traps
Trap 1: "Slope trick works for any convex function."
Not quite. Slope trick works specifically for piecewise linear convex functions -- functions made of straight-line segments joined at breakpoints. A smooth convex function like
Trap 2: "I need to store the entire function f(x) for all x."
No -- that's the whole point of the technique. You store only the breakpoints (the x-values where the slope changes) in two priority queues, plus the minimum value. Everything else is derived. For a function with
WRONG mental model: CORRECT mental model:
"Store f(x) for all x" "Store only breakpoints + min"
f(x): array of size V f(x): two heaps + one scalar
[5, 4, 3, 2, 1, 0, 1, 2, 3] L (max-heap): {5}
^ ^ ^ ^ ^ ^ ^ ^ ^ R (min-heap): {5}
0 1 2 3 4 5 6 7 8 min_val: 0
\___ V = 9 entries ___/
That's it. 2 entries + 1 scalar
Memory: O(V) vs O(V) for the full table.
Memory: O(k) breakpointsTrap 3: "Adding two slope trick functions is complicated."
It's actually the simplest operation conceptually: just merge the two priority queues. If
f(x): g(x): f(x) + g(x):
\ / \ / \\ //
\ / \/ \\ //
\/ \\/
L={2} R={5} L={3} R={7} L={2,3} R={5,7}
min=0 min=0 min=0+0=0
"Merge" = just pour one heap into the other.
For tree DP: use small-to-large merging -> O(n log²n) total.Debug Drills
Drill 1: Forgetting to update minimum when popping
What's wrong?
cpp
// Make array non-decreasing with minimum total |a[i] - x_i| cost
priority_queue<long long> L; // max-heap, left breakpoints
priority_queue<long long, vector<long long>, greater<>> R; // min-heap, right
long long ans = 0;
for (int i = 0; i < n; i++) {
L.push(a[i]);
R.push(a[i]);
if (L.top() > R.top()) {
long long l = L.top(), r = R.top();
L.pop(); R.pop();
L.push(r);
R.push(l);
// BUG: forgot to add (l - r) to ans!
}
}
cout << ans << endl;When a breakpoint l from the left heap exceeds r from the right heap and they swap, the function's minimum increases by l - r. Without ans += l - r, the answer underestimates the true cost. Fix: Add ans += l - r; inside the if-block. Minimum-tracking is not automatic from the heaps alone.
Drill 2: Pushing to only one heap instead of both
What's wrong?
cpp
// Adding cost |x - a[i]|: should push a[i] to BOTH heaps
for (int i = 0; i < n; i++) {
L.push(a[i]);
// BUG: forgot R.push(a[i])!
if (!R.empty() && L.top() > R.top()) {
// ... swap logic ...
}
}|x - a[i]| introduces two breakpoints -- slope changes from whatever to one less on the left of a[i], and from whatever to one more on the right. Pushing to only L adds only the left slope change, making the function non-convex. Fix: Push a[i] to both L and R.
Drill 3: Lazy offset applied incorrectly
What's wrong?
cpp
long long l_offset = 0, r_offset = 0;
void shift(long long delta) {
l_offset += delta;
r_offset += delta;
}
void addAbsValue(long long a) {
L.push(a); // BUG: should push (a - l_offset)!
R.push(a); // BUG: should push (a - r_offset)!
}With lazy offsets, heap values are stored as (actual - offset) so that heap.top() + offset reconstructs the real breakpoint. Inserting a raw a means its effective value becomes a + offset. Fix: L.push(a - l_offset); and R.push(a - r_offset);. Every read/write must be mirrored through the offset.
Self-Test
Before moving to practice problems, verify your understanding:
- [ ] Explain what a "piecewise linear convex function" is and why slope trick represents it with breakpoints instead of storing all values
- [ ] Describe what the two heaps (L and R) store, their heap orientations, and the invariant
all of L <= all of R - [ ] Implement the "add |x - a|" operation on the two-heap representation, including the swap-and-fix step and minimum value update
- [ ] Implement the "shift all breakpoints by δ" operation using the lazy offset trick (O(1) instead of O(n))
- [ ] Walk through the "make array non-decreasing with minimum cost" algorithm on a small example (e.g., a = [3, 1, 4])
- [ ] State the time complexity of slope trick: O(n log n) for n operations, since each element adds O(1) breakpoints and each heap operation is O(log n)
- [ ] Explain why prefix-min corresponds to clearing the right heap R, and suffix-min to clearing the left heap L
Practice Problems
| CF Rating | What you should be comfortable with |
|---|---|
| 1200 | Piecewise-linear convex functions; know that ` |
| 1500 | Store breakpoints in a priority queue; implement "push left / push right"; implement the basic non-decreasing array problem |
| 1800 | Handle shift operations; combine slope trick with prefix-min / suffix-min clipping; solve CSES Increasing Array II and CF 713C |
| 2100 | Merge two slope-trick functions (tree DP); handle three-region piecewise-linear functions; solve CF 1534F2 and ABC 217H |
| # | Problem | Source | Difficulty | Key Idea |
|---|---|---|---|---|
| 1 | CF 713C -- Sonya and Problem Wihtout a Legend | CF 713C | 1900 | Canonical: make non-decreasing, min cost. Direct slope trick or coordinate-compress DP. |
| 2 | CF 1534C -- Little Alawn's Puzzle | CF 1534C | 1400 | Warm-up: cycle counting (not slope trick, but context for 1534F) |
| 3 | CF 1534F2 -- Falling Sand (Hard) | CF 1534F2 | 2100 | Slope trick on a tree with merging |
| 4 | ABC 217H -- Snuketoon | AtCoder | 2100 | Slope trick with three regions; absolute value costs from moving |
| 5 | CF 1779F -- Xorcerer | CF 1779F | 2000 | Slope trick on tree structure |
| 6 | CF 1Amount -- Stock Trading | CF Blog | 2000 | Classic slope trick walkthrough problem |
| 7 | USACO 2016 -- Fencing the Cows (Plat) | USACO | 2000 | Monotone sequence with absolute value cost on 2D variant |
| 8 | CF 1381C -- Mastermind | CF 1381C | 2100 | Greedy + matching but slope-trick-like cost analysis |
| 9 | JOI 2017 -- Spring Campaign (Hiring) | JOI | 2100 | Slope trick with sliding window operations |
| 10 | CF 1083E -- The Fair Nut and Rectangles | CF 1083E | 2100 | Convex piecewise-linear DP (CHT also works; slope trick alternative) |
| 11 | Increasing Array II | CSES | 2100 | Make array non-decreasing with minimum cost -- classic slope trick |
Further Reading
- CF Blog: Slope Trick -- zscoder -- The original and most comprehensive slope trick tutorial.
- CF Blog: Slope Trick Explained -- Nyaan -- Walkthrough with stock trading problem.
- cp-algorithms -- DP Optimizations -- Context on DP speedups.
- Competitive Programmer's Handbook (Laaksonen) -- General DP and convexity background.
- For the CHT alternative to convex DP: see DP -- Convex Hull Trick.
- For divide-and-conquer DP: see DP -- Divide and Conquer Optimization.