Dynamic Programming/Principles of Dynamic Programming

Lesson 8.12,241 words

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 pin down 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 powerful and most misunderstood 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 that cuts through the mystique: 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 and catastrophically slow.

Algorithm 1:Rec-Fib(n)\textsc{Rec-Fib}(n) — naive recursive Fibonacci
  1. 1
    if n<2n < 2 then
  2. 2
    return nn
  3. 3
    return Rec-Fib(n1)\textsc{Rec-Fib}(n-1) + Rec-Fib(n2)\textsc{Rec-Fib}(n-2)

Why is it slow? Because it recomputes the same values an astronomical number of times. The recursion tree for shows the waste already:

Recursion tree for showing repeated subproblems recomputed many times.

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.

Algorithm 2:Memo-Fib(n)\textsc{Memo-Fib}(n) — top-down with a memo table M[0..n]M[0..n]
  1. 1
    if M[n]M[n] is defined then
  2. 2
    return M[n]M[n]
    already solved
  3. 3
    if n<2n < 2 then
  4. 4
    M[n]nM[n] \gets n
  5. 5
    else
  6. 6
    M[n]M[n] \gets Memo-Fib(n1)\textsc{Memo-Fib}(n-1) + Memo-Fib(n2)\textsc{Memo-Fib}(n-2)
  7. 7
    return M[n]M[n]

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.

Memoized : each distinct subproblem is computed once (solid); later requests are cache hits (grey) that return immediately, pruning the repeated subtrees.

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.

Algorithm 3:Tab-Fib(n)\textsc{Tab-Fib}(n) — bottom-up over a table F[0..n]F[0..n]
  1. 1
    F[0]0F[0] \gets 0
  2. 2
    F[1]1F[1] \gets 1
  3. 3
    for i2i \gets 2 to nn do
  4. 4
    F[i]F[i1]+F[i2]F[i] \gets F[i-1] + F[i-2]
  5. 5
    return F[n]F[n]

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.

Top-down and bottom-up fill the same table in opposite orders, yet produce identical values .

The two conditions that make DP work

Dynamic programming is not a universal hammer. It 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, not a bag of tricks. The method distils into an explicit checklist, the key steps in a DP solution, and every example follows the same order. Following it turns a moment of insight into a routine you can execute.

  1. 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.
  2. 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.
  3. 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.
  4. Prove correctness of the equations (induction, usually). Argue by induction on subproblem size that the equation computes exactly the quantity the definition names.
  5. Write the pseudocode (be iterative!). Turn the equations into a bottom-up loop that fills a table in an order respecting the dependencies.
  6. 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.
  7. Argue correctness of the pseudocode (usually very short): it faithfully evaluates the equations in a valid order.
  8. 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.

A worked optimization: weighted interval scheduling

Fibonacci shows the speedup but hides the optimization flavor that gives DP its real power. 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 exactly 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:

Intervals sorted by finish time; is the rightmost interval ending before starts. Here : interval is the last one entirely left of .

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 (sketch). By induction on . The base case is immediate. For the step, any optimal schedule on the first intervals either omits (an optimal schedule on the first , by the inductive hypothesis ) or contains (then the rest is an optimal schedule on , contributing ). Taking the max of the two cases is exactly the equation.

Step 5: Pseudocode. Fill the table in increasing , so both and are ready when needed.

Algorithm 4:Iter-IS(I1,,In)\textsc{Iter-IS}(I_1, \dots, I_n) — max-profit interval scheduling
  1. 1
    sort intervals by increasing finish time fif_i
    O(nlogn)O(n \log n)
  2. 2
    Opt[0]0\textsc{Opt}[0] \gets 0
  3. 3
    for i1i \gets 1 to nn do
  4. 4
    kq(i)k \gets q(i)
  5. 5
    Opt[i]max(Opt[i1], pi+Opt[k])\textsc{Opt}[i] \gets \max\bigl(\textsc{Opt}[i-1],\ p_i + \textsc{Opt}[k]\bigr)
    O(1)O(1) per iteration
  6. 6
    return Opt[n]\textsc{Opt}[n]

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.

Dependency graph of interval-scheduling subproblems sharing overlapping entries.

Several nodes (, , ) are pointed to more than once: those are the overlapping subproblems the table solves just once.

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 .

Algorithm 5:Iter-IS(I1,,In)\textsc{Iter-IS}^{\prime}(I_1, \dots, I_n) — also reconstruct the set
  1. 1
    sort intervals by increasing finish time fif_i
  2. 2
    Opt[0]0\textsc{Opt}[0] \gets 0
  3. 3
    allocate boolean array chosen[1..n]\textit{chosen}[1..n]
  4. 4
    for i1i \gets 1 to nn do
  5. 5
    kq(i)k \gets q(i)
  6. 6
    if Opt[i1]pi+Opt[k]\textsc{Opt}[i-1] \ge p_i + \textsc{Opt}[k] then
  7. 7
    chosen[i]false\textit{chosen}[i] \gets \text{false}
  8. 8
    Opt[i]Opt[i1]\textsc{Opt}[i] \gets \textsc{Opt}[i-1]
  9. 9
    else
  10. 10
    chosen[i]true\textit{chosen}[i] \gets \text{true}
  11. 11
    Opt[i]pi+Opt[k]\textsc{Opt}[i] \gets p_i + \textsc{Opt}[k]
  12. 12
    return Opt[n], chosen\textsc{Opt}[n],\ \textit{chosen}

Trace back from : if , output and jump to ; otherwise step to . This walk recovers an optimal set in .

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 12345678910
price 1589101717202430

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.

Algorithm 6:Cut-Rod(p[1..n])\textsc{Cut-Rod}(p[1..n]) — maximum revenue cutting a length-nn rod
  1. 1
    Opt[0..n]0\textsc{Opt}[0..n] \gets 0
  2. 2
    for i1i \gets 1 to nn do
  3. 3
    for j1j \gets 1 to ii do
  4. 4
    Opt[i]max(Opt[i], p[j]+Opt[ij])\textsc{Opt}[i] \gets \max\bigl(\textsc{Opt}[i],\ p[j] + \textsc{Opt}[i-j]\bigr)
    rightmost piece length jj
  5. 5
    return Opt[n]\textsc{Opt}[n]

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 .

Algorithm 7:Print-Cut-Rod(p[1..n])\textsc{Print-Cut-Rod}(p[1..n]) — print an optimal set of cuts
  1. 1
    opt,rightmostCut-Rod(p)\textit{opt}, \textit{rightmost} \gets \textsc{Cut-Rod}^{\prime}(p)
    also returns cut lengths
  2. 2
    while n>0n > 0 do
  3. 3
    print rightmost[n]\textit{rightmost}[n]
  4. 4
    nnrightmost[n]n \gets n - \textit{rightmost}[n]

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

  1. Erickson, Ch. 3 — Dynamic Programming: the working definition of dynamic programming as recursion without repetition.
  2. Skiena, §10 — Dynamic Programming: top-down memoization as caching results inside the natural recursion.
  3. CLRS, Ch. 15 — Dynamic Programming: the optimal-substructure cut-and-paste test for a correct recurrence.
Practice

╌╌ END ╌╌