v9.1.1
Menu

Chapter 10: Dynamic Programming Basics

Week 10

Master the fundamentals of dynamic programming: recognizing overlapping subproblems, building optimal substructure, and choosing between memoization and tabulation. Apply these principles to classic 1D and 2D DP problems.

Topics
7
Demos
4
Videos
3
Min read
36

Chapter Overview

Dynamic Programming (DP) is one of the most powerful algorithmic techniques for solving optimization problems. At its core, DP is about breaking a problem into smaller overlapping subproblems, solving each one only once, and storing the results for reuse.

Two key properties make a problem amenable to DP:

  • Overlapping subproblems: The same subproblems appear multiple times during computation (unlike divide and conquer, where subproblems are independent)
  • Optimal substructure: The optimal solution to the problem can be constructed from optimal solutions to its subproblems

There are two main approaches to implementing DP:

  • Memoization (top-down): Start from the original problem, recurse into subproblems, and cache results as you go
  • Tabulation (bottom-up): Build a table from the smallest subproblems upward, filling in solutions iteratively

This chapter covers:

  • DP fundamentals: Overlapping subproblems, optimal substructure, memoization, and tabulation
  • 1D DP problems: Climbing stairs, house robber
  • 2D DP problems: Grid-based DP, unique paths, minimum path sum
  • Pattern recognition: How to identify and formulate DP state transitions

Chapter Roadmap

Click any topic to jump in

Explainer videoInteractive demo
1
DP Introduction

Overlapping subproblems, optimal substructure, memoization vs tabulation.

Overlapping SubproblemsOptimal SubstructureMemoization vs Tabulation
Multiple approaches

Each tackles a different aspect

2
Climbing Stairs

The classic 1D recurrence: $dp[i] = dp[i-1] + dp[i-2]$, Fibonacci in disguise.

State Definition and Base CasesRecognizing Fibonacci Structure
3
House Robber

Include/exclude pattern with adjacency constraint: $dp[i] = \max(dp[i-1], dp[i-2] + a_i)$.

State Transition with ConstraintsInclude/Exclude Pattern
Builds on previous
4
Grid-Based DP

2D state design and row-by-row filling order with clear dependencies.

2D State DefinitionFilling Order and Dependencies
Multiple approaches

Each tackles a different aspect

5
Unique Paths

Counting lattice paths: $dp[i][j] = dp[i-1][j] + dp[i][j-1]$ and its binomial closed form.

Counting Paths with DPHandling Obstacles
6
Minimum Path Sum

Weighted grid DP with in-place $O(1)$ extra space optimization.

DP on Weighted GridsIn-Place DP Optimization
Builds on previous
7
Maximal Square

State as side length with the $\min$-of-three-neighbors recurrence.

State as Square Side LengthWhy min() of Three Neighbors?

Topics

7 topics in this chapter

Write the obvious recursive function for the 40th Fibonacci number and Python needs most of a minute; ask for the 60th and it will not finish. The recursion is correct, but it keeps solving the same small questions. The call for fib(40) asks for fib(39) and fib(38), fib(39) asks for fib(38) again, and the repetition compounds until hundreds of millions of calls are spent on only 41 distinct values.

The previous chapter, Heaps & Priority Queues, ended with Find Median from Data Stream, where the right data structure made each query cheap. Dynamic programming makes the same move for recursion: store each subproblem's answer the first time it is computed and reuse it every time it is needed again. Two properties decide whether this works. Overlapping subproblems means the recursion revisits the same inputs, so caching saves work. Optimal substructure means the best answer is built from best answers to smaller inputs, so a recurrence exists at all.

We first count the repeated calls that make naive recursion exponential. Then we test optimal substructure, including a problem where it fails, and compare the two ways to implement a recurrence: top-down memoization and bottom-up tabulation.

Definition

Dynamic programming solves a problem by expressing its answer as a recurrence over smaller subproblems, solving each distinct subproblem once, and storing the result for reuse. It applies when subproblems overlap and the problem has optimal substructure. Running time is the number of distinct states multiplied by the work per state.

In this topic

  1. 1Overlapping Subproblems
  2. 2Optimal Substructure
  3. 3Memoization vs Tabulation
1 of 3
Overlapping Subproblems

Divide and conquer, as in merge sort, splits a problem into disjoint halves that never share work. Many recursions do share work. A problem has overlapping subproblems when its recursion calls the same input many times. Naive Fibonacci is the standard case: fib(n) calls fib(n-1) and fib(n-2), and fib(n-1) calls fib(n-2) again. The call count T(n)=T(n−1)+T(n−2)+1T(n) = T(n-1) + T(n-2) + 1 grows like φn\varphi^n, where φ≈1.618\varphi \approx 1.618 is the golden ratio, yet only n+1n + 1 distinct inputs exist. Caching each answer once collapses the exponential call tree to O(n)O(n) work. Without overlap, a cache only adds memory.

Mathematical Intuition

Naive recursion on a problem like Fibonacci generates a recursion tree of size T(n)=T(n−1)+T(n−2)+O(1)T(n) = T(n-1) + T(n-2) + O(1), which solves to T(n)=O(φn)T(n) = O(\varphi^n) with φ=(1+5)/2≈1.618\varphi = (1+\sqrt{5})/2 \approx 1.618. But the number of distinct subproblems is only n+1n+1. Caching each unique result once collapses the exponential tree into a DAG with O(n)O(n) nodes and O(n)O(n) edges, giving linear time.

Example:

How many times is fib(2) computed in a naive recursive call to fib(6)?

2 of 3
Optimal Substructure

Caching only helps if the answer can be assembled from answers to smaller problems. A problem has optimal substructure when an optimal solution contains optimal solutions to its subproblems. Shortest paths have it: if the shortest route from A to C passes through B, its A-to-B part must be a shortest A-to-B route, or swapping in a shorter one would improve the whole route, a contradiction. That cut-and-paste argument is how you check any recurrence. It fails when subproblems interact, for example when both parts compete for the same resource. Longest simple path is the classic failure: the two halves may reuse a vertex, so it has no efficient DP.

Mathematical Intuition

Optimal substructure says an optimal solution to a problem contains optimal solutions to its subproblems. Formally, if f(x)=opty∈N(x)g(f(y),x,y)f(x) = \text{opt}_{y \in N(x)} g(f(y), x, y) where gg is monotone in its first argument, then solving the subproblems optimally is sufficient. This is the prerequisite for any valid DP recurrence and distinguishes DP from problems where local optima can trap you away from the global optimum.

Example:

Does the longest simple path problem have optimal substructure?

3 of 3
Memoization vs Tabulation

Once a recurrence is known, there are two ways to evaluate it. Memoization (top-down) keeps the recursive function and adds a cache: check the cache, and compute and store only on a miss. It evaluates only the states the recursion actually reaches, but each pending call occupies stack space, and Python stops at about 1000 frames. Tabulation (bottom-up) allocates a table, fills base cases, then loops through states in an order where every dependency is already filled. It visits every state, has no recursion, and makes it easy to keep only the last few rows. Both cost the number of distinct states multiplied by the work per state.

Mathematical Intuition

Both approaches solve the same DAG of subproblems, so they share asymptotic complexity O(states×transitions)O(\text{states} \times \text{transitions}). Memoization (top-down) has O(depth)O(\text{depth}) stack overhead and only computes reachable states — useful when the reachable set is much smaller than the full state space. Tabulation (bottom-up) visits every state but avoids recursion overhead and enables rolling-array space optimization like O(W)O(W) instead of O(nW)O(nW) for knapsack.

Example:

When would you prefer memoization over tabulation?