Principles of Dynamic Programming
Dynamic programming is recursion with memory: when a recursive solution re-solves the same subproblems again and again, we solve each one once and store the answer. We identify the two structural conditions that make this work — overlapping subproblems and optimal substructure — contrast top-down memoization with bottom-up tabulation, and distil the whole method into a five-step recipe.
╌╌╌╌
Dynamic programming is one of the most broadly applicable ideas in algorithm design. The name, coined by Richard Bellman in the 1950s, is a historical accident: it has nothing to do with dynamics and little to do with programming in the modern sense. Erickson gives a one-line definition: dynamic programming is recursion without repetition.1 We find a recursive structure for the problem, notice that the naive recursion solves the same subproblems over and over, and then arrange to solve each distinct subproblem exactly once, remembering its answer.
That single move, trading repeated computation for stored results, can collapse an exponential running time to a polynomial one. The rest is bookkeeping: identifying the subproblems, writing the recurrence that relates them, and deciding the order in which to fill in the answers.
A motivating disaster: Fibonacci
The Fibonacci numbers are defined by the recurrence
Transcribed directly into a recursive procedure, this definition is correct but exponentially slow.
- 1if then
- 2return
- 3return +
It is slow because it recomputes the same values exponentially many times. The recursion tree for shows the waste already:
The subtree rooted at appears twice; appears three times; and the duplication compounds with depth. The number of leaves is itself , and since the Fibonacci numbers grow like (with the golden ratio), runs in time: exponential, to compute a quantity we could write down in a fraction of a second.
The diagnosis is precise: there are only distinct subproblems, , but the recursion visits them exponentially often.
The first cure: memoization (top-down)
The minimal fix keeps the recursive structure but adds a memo, a table that remembers each answer the first time we compute it. Before recursing, we check the table; if the answer is there, we return it immediately.
- 1if is defined then
- 2returnalready solved
- 3if then
- 4
- 5else
- 6+
- 7return
Now each of the subproblems is solved once; every later request for it is a single table lookup. The running time drops from exponential to . This style, recurse as before but cache results, is called memoization (note: memo-ization, not memorization). It is the top-down form of dynamic programming, and it is often the easiest to write, because it is just the natural recursion plus a guard.2
Compare the recursion tree now against the naive one above: every repeated subproblem becomes a cache hit that returns at once, so the exponential tree collapses to a thin spine of first-time computations.
The greyed nodes (, , ) are the duplicates from the naive tree; here they resolve in a single lookup, so the whole computation touches each of exactly once.
The second cure: tabulation (bottom-up)
If we know in advance which subproblems we need and in what order their dependencies resolve, we can drop the recursion entirely and fill the table directly with a loop. This is tabulation, the bottom-up form.
- 1
- 2
- 3for to do
- 4
- 5return
The loop visits subproblems in an order, , that guarantees every dependency is ready before it is needed. No recursion, no memo-check overhead, no risk of stack overflow.
The two cures fill the same array; they differ only in the order they visit its cells. Bottom-up sweeps the indices forward; top-down dives to first and writes each cell as the recursion unwinds.
The two conditions that make DP work
Dynamic programming applies precisely when a problem has two structural properties, both emphasized by all three texts.
1. Overlapping subproblems. The recursive solution revisits the same subproblems repeatedly; there are only polynomially many distinct ones, even though the naive recursion calls them exponentially often. This is what makes caching pay off. (Contrast divide-and-conquer like merge sort, where each recursive call is on a fresh subproblem; there is nothing to cache, so memoization buys nothing.)
2. Optimal substructure. An optimal solution to the problem is built from optimal solutions to its subproblems. This is what lets us write a recurrence at all: we can express the best answer for an instance in terms of the best answers for smaller instances. CLRS states the test sharply: cut an optimal solution at some choice point; the piece that remains must itself be an optimal solution to the residual subproblem, or we could splice in a better piece and improve the whole, a contradiction.3
The dynamic-programming recipe
DP is a design discipline. The method distils into an explicit checklist, the key steps in a DP solution, and every example follows the same order.
- Identify a simplified goal (maybe). Often the original problem asks for an optimal object (the actual set of cuts, the actual chosen intervals). First solve the easier problem of computing only the optimal value; recovering the object is a separate, usually short, step at the end.
- Clearly define the subproblems; set up notation. State precisely what quantity denotes, in one English sentence, and which call gives the final answer. This is the single hardest and most important step. Get the subproblem definition right and everything else follows; get it wrong and nothing will.
- Write the DP equations. Express of a subproblem in terms of of smaller subproblems, together with the base case(s). This is where optimal substructure is used: enumerate the choices an optimal solution could make at its first decision point and take the best.
- Prove correctness of the equations (induction, usually). Argue by induction on subproblem size that the equation computes the quantity the definition names.
- Write the pseudocode (be iterative!). Turn the equations into a bottom-up loop that fills a table in an order respecting the dependencies.
- Get back to the original goal (maybe). If a simplified goal was used, reconstruct the optimal object, either by storing the winning choice at each entry, or by tracing back through the filled table.
- Argue correctness of the pseudocode (usually very short): it faithfully evaluates the equations in a valid order.
- Analyze the running time. Almost mechanical: it is the number of subproblems times the work per subproblem (the time to evaluate one line of the recurrence).
The rest of this lesson runs the recipe end-to-end on two opening examples: first weighted interval scheduling, then rod cutting.
Deriving the four ingredients
Four decisions turn a problem into a dynamic program: the state, the recurrence, the base case, and the evaluation order. They are not independent guesses; each one constrains the next.
- State. Ask what a subproblem needs to know about the past to make its next decision. Every piece of that information becomes an index. Rod cutting needs only the remaining length, so one index suffices; if a second parameter (a budget, a previous choice, a position in a second string) also mattered, the state would grow a second index. The rule of thumb: the state must be a sufficient summary — two inputs that lead to the same future optimum should map to the same state.
- Recurrence. Fix the state, then name the first (or last) decision an optimal solution makes and enumerate its possible values. Each value leaves a smaller instance whose optimum you already trust; combine the immediate reward with that optimum and take the best. Interval scheduling has one binary decision (take or not); rod cutting has decisions (the length of the rightmost piece).
- Base case. Read it off the smallest states where no decision remains — the empty rod, the empty interval prefix — usually a value of or .
- Evaluation order. Any linear order in which every state precedes the states that depend on it works. When the state is a single index and the recurrence reaches only smaller indices, plain increasing order suffices; multi-index states fill in the order of a topological sort of the dependency DAG.
A worked optimization: weighted interval scheduling
Fibonacci shows the speedup but not the optimization structure that DP is mostly used for. The first optimization example is weighted interval scheduling, which sharpens the greedy interval-scheduling problem you have already met: now each interval carries a profit, and we want the most profitable compatible set rather than merely the most intervals.
Input. Intervals , each with a profit . Desired output. A subset that is feasible (the chosen intervals are pairwise disjoint) and maximizes .
Step 1: Simplified goal. Compute only the maximum achievable profit ; we recover the actual subset afterwards.
Step 2: Subproblems and notation. The key preprocessing move is to sort the intervals by increasing finish time, so . Once sorted, every subproblem we ever need has the contiguous prefix form , so a single index names it. Define
and we want . For each we also precompute
the index of the rightmost interval to the left of that does not overlap
(and if none exists). Because finish times are sorted, ends before starts
is the test, so is well defined.
Drawn on a timeline, the intervals stack up by finish time, and is just the last bar that clears 's left edge:
Step 3: DP equations. Consider the last interval and make one binary choice — include it or not:
If we exclude , the best we can do is . If we include it, we collect and may no longer use any interval that overlaps ; the remaining usable intervals are exactly , so we add — optimal substructure in action.
Step 4: Correctness.
Step 5: Pseudocode. Fill the table in increasing , so both and are ready when needed.
- 1sort intervals by increasing finish time
- 2
- 3for to do
- 4
- 5per iteration
- 6return
Step 6: Back to the original goal. To recover the actual set, record at each which branch won. In the pseudocode below, means is in the optimal solution for the prefix .
- 1sort intervals by increasing finish time
- 2
- 3allocate boolean array
- 4for to do
- 5
- 6if then
- 7
- 8
- 9else
- 10
- 11
- 12return
Trace back from : if , output and jump to ; otherwise step to . This walk recovers an optimal set in .
Step 7: Correctness of the pseudocode. The loop is a faithful transcription of the DP equation: at iteration it reads and , takes the larger of the exclude value and plus the include value, and stores it in . Both entries it reads were filled on an earlier iteration, since and , so the increasing- order respects every dependency. Composed with the Step 4 induction, each holds the true optimum, and is the answer.
Step 8: Running time. The sort costs . Each of the table entries does work given . The values themselves can be found by binary search ( each) or, since they are monotone in , by a single linear scan that advances across all iterations in total. Either way the sort dominates, for a worst-case running time of
This is the canonical include-or-exclude
DP, and its dependency structure makes
the overlap concrete: entry points back to its two predecessors,
and , and those arrows cross and reconverge
on shared subproblems, the same overlap that doomed naive Fibonacci.
Several nodes (, , ) are pointed to more than once: those are the overlapping subproblems the table solves just once.
A second worked optimization: rod cutting
The same recipe applied to rod cutting (CLRS's opening case) shows a different
flavor of choice — not binary, but try every first piece.
Given a rod of integer
length and a price for a piece of length , cut the rod into integer
pieces to maximize total revenue. With the price table
| length | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| price | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
a rod of length sells whole for , but cutting it as earns — so the cuts matter.
Simplified goal. Find just the maximum obtainable revenue. Subproblem. Let be the max revenue from a rod of length ; we want . DP equations. Consider the rightmost cut. If the rightmost piece has length (for some ), we earn and cut the remaining length optimally — optimal substructure. We do not know the best , so we try them all:
The subproblems are ordered by length, so we fill them in increasing order.
- 1
- 2for to do
- 3for to do
- 4rightmost piece length
- 5return
Filling the table by hand. With the price list above, the loop fills from left to right. Each entry takes the best over every rightmost piece length ; the winning is what records for the reconstruction pass.
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 5 | 8 | 10 | 13 | 17 | 18 | 22 | |
| — | 1 | 2 | 3 | 2 | 2 | 6 | 1 | 2 |
Read one entry to see the enumeration. For the four candidates are
won by , so : cut a length- piece and solve the length- remainder optimally. Every entry to the right reuses the entries to its left — each feed several later rows, which is the overlap that makes the table pay off.
Running time. There are subproblems, and the work to compute is at most for a constant (the inner loop runs times). Summing,
a polynomial replacement for the ways to cut the rod that a naive recursion would explore. To recover the cuts, store the winning length in a second array , then read them off by following .
- 1also returns cut lengths
- 2while do
- 3print
- 4
The overlap that makes tabulation worthwhile is visible in the subproblem dependency graph. Each length depends on every shorter length, so the low-index entries are reused again and again — a naive recursion would recompute each of them an exponential number of times, while the table computes each once.
Common pitfalls
A handful of mistakes account for most broken dynamic programs.
- A state that is not a sufficient summary. If two instances with the same state can have different optimal futures, the recurrence is unsound: the table entry conflates cases that should be distinguished. To address this, add the missing parameter to the state, even at the cost of a larger table.
- An evaluation order that reads unfilled cells. Bottom-up code that visits states before their dependencies computes on garbage. Always confirm that the recurrence for a state reaches only states that come earlier in the fill order.
- Confusing optimal substructure with greedy choice. Optimal substructure says the sub-solutions are optimal, not that a locally best first move is globally best. Rod cutting enumerates every first piece precisely because no single greedy cut is always right.
- Assuming DP where subproblems do not overlap. Merge sort splits into disjoint halves; memoizing it caches entries that are each hit once, so it gains nothing over plain divide-and-conquer. DP helps only when the same subproblem recurs.
- Forgetting the reconstruction bookkeeping. Computing the optimal value is half the job; if the problem asks for the optimal object, record the winning choice at each state (as and do) so the trace-back can rebuild it.
Where the name and the method come from
The name dynamic programming
is a historical accident worth knowing. Richard
Bellman coined it in the 1950s at RAND while working on multistage decision
processes; programming
meant planning (as in linear programming
), not writing
code, and Bellman later admitted in his autobiography that he chose dynamic
partly because it was impossible to use pejoratively and would shield the research
from a skeptical Secretary of Defense. The mathematical core is Bellman's
principle of optimality — an optimal policy has the property that whatever the
initial state and decision, the remaining decisions must be optimal with respect to
the state that results — the optimal-substructure condition itself, stated
for sequential decision problems. Bellman's Dynamic Programming (1957) is the
founding text.4
That decision-process lineage leads directly to
reinforcement learning. The Bellman equation is the infinite-horizon, probabilistic
generalization of the recurrences in this lesson — value iteration is tabulation,
and the whole field of approximate dynamic programming exists because the state
space is too large to fill a table (Bertsekas, Dynamic Programming and Optimal
Control). The number of subproblems work per subproblem
cost model also
has a modern lower-bound counterpart: for some classic DPs the quadratic running time is
provably near-optimal. Backurs and Indyk (2015) showed that edit distance and
several sequence DPs cannot be solved in strongly subquadratic
time unless the Strong Exponential Time Hypothesis fails — so the table
of the next lesson is
essentially the best one can hope for.
Takeaways
- Dynamic programming is recursion without repetition: find a recursive structure, then solve each distinct subproblem once and store the result.
- It applies exactly when the problem has overlapping subproblems (so caching helps) and optimal substructure (so a recurrence over subproblems is correct).
- Memoization (top-down) caches results inside the natural recursion; tabulation (bottom-up) fills the table with a loop in dependency order. Same values, same time; tabulation often saves space.
- The DP recipe develops every dynamic program systematically: simplified goal → define subproblems/notation → DP equations → prove correctness by induction → iterative pseudocode → recover the original object → runtime.
- The hardest step is defining the subproblem; a smart preprocessing choice can make it cheap: sorting intervals by finish time reduces every subproblem to a prefix named by one index.
- Running time (number of subproblems) (work per subproblem): Fibonacci becomes , weighted interval scheduling , rod cutting .
Footnotes
- Erickson, Ch. 3 — Dynamic Programming: the working definition of dynamic programming as
recursion without repetition.
↩ - Skiena, §10 — Dynamic Programming: top-down memoization as caching results inside the natural recursion. ↩
- CLRS, Ch. 15 — Dynamic Programming: the optimal-substructure cut-and-paste test for a correct recurrence. ↩
- Bellman, Dynamic Programming (1957): the principle of optimality and the origin of the term. Backurs & Indyk (2015, STOC): edit distance has no strongly subquadratic algorithm unless SETH fails, a conditional lower bound matching the table. ↩
╌╌ END ╌╌