Skip to content
Part IA Lent Term

Dynamic Programming: Introduction

Motivation

Consider the Fibonacci numbers: F0=F1=1F_0 = F_1 = 1, Fn=Fn1+Fn2F_n = F_{n-1} + F_{n-2} for n2n \ge 2. A naive recursive implementation:

def f(n):
    if n < 2: return 1
    return f(n-2) + f(n-1)

The call tree is exponential: computing F5F_5 calls F4F_4 and F3F_3; computing F4F_4 also calls F3F_3, etc. The same subproblems are recomputed many times. The running time is Θ(2n)\Theta(2^n) — utterly impractical for n>50n > 50.

Dynamic programming (DP) solves this by storing computed results and reusing them. The same Fibonacci computation drops to Θ(n)\Theta(n).

When to Use DP

Two properties must hold:

  1. Optimal substructure: An optimal solution to the problem contains within it optimal solutions to subproblems. Given a choice at each step, the best overall choice is composed of locally best choices.

  2. Overlapping subproblems: The recursive solution revisits the same subproblems repeatedly. DP stores their solutions so each is solved only once.

Divide and conquer also exploits optimal substructure but assumes subproblems are disjoint (no overlap). DP handles the overlapping case.

Two Approaches

Top-Down (Memoisation)

Write the recursive solution. Before computing any subproblem, check a cache (memo table). If the result exists, return it; otherwise compute, cache, and return.

cache = {}
def f(n):
    if n in cache: return cache[n]
    if n < 2: res = 1
    else: res = f(n-2) + f(n-1)
    cache[n] = res
    return res
  • Solves only subproblems actually needed.
  • Requires stack space for recursion (depth proportional to nn for Fibonacci).

Bottom-Up (Tabulation)

Identify the dependency order of subproblems and fill the table iteratively from base cases upward.

def f(n):
    x = [1] * (n+1)
    for i in range(2, n+1):
        x[i] = x[i-2] + x[i-1]
    return x[n]
  • Solves all subproblems up to nn, even ones not strictly needed.
  • No recursive stack overhead.
  • Easier to analyse space and time.

Space can often be optimised. For Fibonacci, only the last two values are needed:

def f(n):
    x, y = 1, 1
    for _ in range(2, n+1):
        x, y = y, x + y
    return y

This reduces space from Θ(n)\Theta(n) to Θ(1)\Theta(1).

DP vs. Divide and Conquer

AspectDivide & ConquerDynamic Programming
Subproblem overlapDisjoint (no overlap)Significant overlap
ApproachRecursive, no cacheRecursive with cache (memoisation) or iterative (bottom-up)
ExamplesMergesort, quicksort, binary searchFibonacci, LCS, matrix chain, rod cutting
Time saved by DPNone (no overlap to exploit)Exponential → polynomial

The Naive Bellman Recursion Problem

General optimisation problems framed via the Bellman equation (choose optimal sequence of actions) suffer from the same exponential blow-up as Fibonacci when implemented naively. The computation tree grows exponentially with problem size, because the same states are reached through many different action sequences. DP is precisely the remedy.

Summary

PropertyValue
RequirementsOptimal substructure + overlapping subproblems
Top-downRecursion + memoisation cache
Bottom-upIterative table filling from base cases
Typical speed-upExponential → polynomial
Space vs. timeOften can reduce space by discarding unneeded table entries