<< Home

Chapter 5: Dynamic Programming

Dynamic programming, often abbreviated as DP, is a powerful problem-solving technique used to optimise decisions that unfold over multiple stages. Instead of solving the same subproblems repeatedly, DP stores their results and reuses them. This makes it an essential tool for problems where naive recursion would otherwise cause exponential work.

💡 The term “dynamic programming” was coined by Richard Bellman in the 1950s, not for computers but for solving optimisation problems in mathematics and economics. The “programming” part refers to planning, not writing software.

Recognizing Overlapping Subproblems

The first clue that a problem can be approached with dynamic programming is the presence of overlapping subproblems, which are smaller instances that recur during computation. When a recursive approach recomputes the same results multiple times, dynamic programming can make it efficient by caching or tabulating those results.

Understanding problem structure

Dynamic programming relies on two structural features: optimal substructure and overlapping subproblems. Optimal substructure means that the best solution to a larger problem depends on the best solutions to its smaller parts. Overlapping subproblems mean those parts are reused across branches of the computation tree.

A simple recursive example

The Fibonacci sequence illustrates overlapping subproblems clearly. The naive recursive version of fib(n) calls fib(n-1) and fib(n-2), both of which call smaller values repeatedly, causing exponential growth in calls.

def fib(n):
  if n <= 1:
    return n
  return fib(n - 1) + fib(n - 2)
⚠️ For even moderate n, this function becomes extremely slow because it recalculates many of the same values. For example, fib(35) calls fib(34) and fib(33), both of which call fib(32), and so on.

Memoization and Tabulation

Dynamic programming improves on naive recursion by storing partial results. There are two main techniques: memoization (top-down) and tabulation (bottom-up). Both rest on the same idea, which is to reuse what you already know instead of recomputing it.

Top-down memoization

Memoization adds a cache that records the answer for each argument the first time it is computed. Subsequent calls reuse the cached value instead of redoing the computation.

def fib_memo(n, cache=None):
  if cache is None:
    cache = {}
  if n in cache:
    return cache[n]
  if n <= 1:
    result = n
  else:
    result = fib_memo(n - 1, cache) + fib_memo(n - 2, cache)
  cache[n] = result
  return result

Bottom-up tabulation

Tabulation reverses the process. It starts from the smallest subproblems and builds upward until reaching the desired result. This version avoids recursion and often runs even faster.

def fib_tab(n):
  if n <= 1:
    return n
  table = [0, 1]
  for i in range(2, n + 1):
    table.append(table[i - 1] + table[i - 2])
  return table[n]
💡 In practice, the choice between memoization and tabulation depends on the problem. Memoization is often easier to implement recursively, while tabulation is preferred for strict time control and iteration over large states.

Example: Fibonacci and Knapsack

Two classic examples show dynamic programming in action: the Fibonacci sequence and the 0/1 knapsack problem. Fibonacci demonstrates the principle of reuse. Knapsack shows how to optimise resource allocation.

Fibonacci revisited

Both memoization and tabulation produce the same results for Fibonacci numbers. However, the iterative tabulation version uses only O(n) time and space. This demonstrates how DP eliminates redundant work.

Solving the 0/1 knapsack problem

In the 0/1 knapsack problem, each item has a weight and a value. The goal is to fill a knapsack of limited capacity with items that maximise total value without exceeding the weight limit. A greedy strategy often fails, but dynamic programming guarantees an optimal result.

def knapsack(weights, values, capacity):
  n = len(weights)
  dp = [[0] * (capacity + 1) for _ in range(n + 1)]

  for i in range(1, n + 1):
    w = weights[i - 1]
    v = values[i - 1]
    for c in range(capacity + 1):
      if w <= c:
        dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - w] + v)
      else:
        dp[i][c] = dp[i - 1][c]
  return dp[n][capacity]
⚠️ The 0/1 knapsack problem cannot be solved greedily by taking the highest value-to-weight ratio first. Only by exploring all feasible combinations of items, as dynamic programming does, can we be certain of finding the best total value.

Optimizing Paths and Resource Usage

Dynamic programming extends naturally to graphs and networks where we want to minimise cost or maximise efficiency. Many pathfinding and scheduling problems use DP ideas beneath the surface, even when they appear very different on the surface.

Shortest paths in directed acyclic graphs

When a graph has no cycles, shortest paths can be computed efficiently by processing vertices in topological order. Each vertex’s distance depends only on those before it, so no revisiting is needed.

def shortest_dag(n, edges, source):
  # edges: list of (u, v, w)
  adj = [[] for _ in range(n)]
  for u, v, w in edges:
    adj[u].append((v, w))

  # simple topological sort
  indeg = [0] * n
  for u, v, _ in edges:
    indeg[v] += 1
  order = [i for i in range(n) if indeg[i] == 0]
  for u in order:
    for v, _ in adj[u]:
      indeg[v] -= 1
      if indeg[v] == 0:
        order.append(v)

  dist = [float("inf")] * n
  dist[source] = 0
  for u in order:
    for v, w in adj[u]:
      if dist[u] + w < dist[v]:
        dist[v] = dist[u] + w
  return dist

Resource allocation and scheduling

DP is widely used to allocate limited resources across competing tasks. Examples include task scheduling, project planning, and optimal investment decisions. Each stage builds on prior results, balancing cost and benefit as it proceeds.

💡 Dynamic programming is a natural choice when a problem can be expressed as “a sequence of decisions that depend on previous outcomes.” Once this structure is identified, a DP formulation usually follows.

Dynamic Programming in Everyday Scenarios

Outside pure computer science, dynamic programming principles show up in many fields, from finance to robotics to daily planning. Any process that breaks a large decision into smaller linked choices can benefit from this structured approach.

Route planning and navigation

Modern navigation software uses DP principles to compute efficient paths while adapting to real-time conditions. Algorithms such as Dijkstra’s or A* effectively reuse partial path costs and update them dynamically as new data arrives.

Planning and optimisation in daily life

Even routine decisions can mirror DP logic. When budgeting time or money, we naturally make local decisions that contribute to a long-term goal, often revising plans based on what we have already done. Recognising this recursive structure is what makes dynamic programming such an intuitive yet profound idea.