Chapter 10: Dynamic Programming Basics
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
DP Introduction
Overlapping subproblems, optimal substructure, memoization vs tabulation.
Each tackles a different aspect
Climbing Stairs
The classic 1D recurrence: $dp[i] = dp[i-1] + dp[i-2]$, Fibonacci in disguise.
House Robber
Include/exclude pattern with adjacency constraint: $dp[i] = \max(dp[i-1], dp[i-2] + a_i)$.
Grid-Based DP
2D state design and row-by-row filling order with clear dependencies.
Each tackles a different aspect
Unique Paths
Counting lattice paths: $dp[i][j] = dp[i-1][j] + dp[i][j-1]$ and its binomial closed form.
Minimum Path Sum
Weighted grid DP with in-place $O(1)$ extra space optimization.
Maximal Square
State as side length with the $\min$-of-three-neighbors recurrence.
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
- 1Overlapping Subproblems
- 2Optimal Substructure
- 3Memoization vs Tabulation
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 grows like , where is the golden ratio, yet only distinct inputs exist. Caching each answer once collapses the exponential call tree to work. Without overlap, a cache only adds memory.
Naive recursion on a problem like Fibonacci generates a recursion tree of size , which solves to with . But the number of distinct subproblems is only . Caching each unique result once collapses the exponential tree into a DAG with nodes and edges, giving linear time.
How many times is fib(2) computed in a naive recursive call to fib(6)?
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.
Optimal substructure says an optimal solution to a problem contains optimal solutions to its subproblems. Formally, if where 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.
Does the longest simple path problem have optimal substructure?
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.
Both approaches solve the same DAG of subproblems, so they share asymptotic complexity . Memoization (top-down) has 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 instead of for knapsack.
When would you prefer memoization over tabulation?
Related Problems on PixelBank
You climb a staircase of steps, taking either one or two steps at a time, and you want to know how many different sequences of moves reach the top. Listing them works for five steps, where there are eight, but the count roughly multiplies by 1.6 with every extra step, so enumeration fails long before reaches 100.
The previous topic, DP Introduction, set out the ideas in the abstract: overlapping subproblems, optimal substructure, and the choice between memoization and tabulation. Climbing Stairs is the first full problem to apply them, and it shows the step every DP solution starts with, defining the state. Let count the ways to stand on step . Every way ends with a final move, either one step from or two steps from . These two groups do not overlap and together cover every way, so their counts add. The result is the Fibonacci recurrence, with no search at all.
We first set up the state and base cases and fill the table for a small staircase. Then we recognise the same shape in variants with different step sizes, and see how the recurrence changes when the allowed moves change.
Definition
Climbing Stairs counts the distinct ordered sequences of 1-step and 2-step moves that sum to exactly . With the number of ways to reach step , and , so is the Fibonacci number . Tabulation runs in time and extra space.
In this topic
- 1State Definition and Base Cases
- 2Recognizing Fibonacci Structure
State Definition and Base Cases
Every DP starts by choosing what one table entry means. Here is the number of distinct move sequences that end exactly on step . To find a recurrence, split those sequences by their last move: a final 1-step comes from step and a final 2-step comes from step . The groups are disjoint and cover every sequence, so . Base cases stop the recursion: counts the empty sequence, and . A wrong base case, such as , shifts or zeroes the whole table even when the recurrence is right.
Define = number of ways to reach step . The recurrence with base cases corresponds exactly to the Fibonacci sequence shifted by one. Correctness follows from partitioning paths by their last step: either we took a 1-step from or a 2-step from , and these sets are disjoint and exhaustive.
How many ways can you climb a staircase with 5 steps?
Recognizing Fibonacci Structure
Many one-dimensional problems share this shape: the current answer depends on a fixed number of earlier answers. When the allowed moves form a set of step sizes, splitting by the last move gives , with . For this is Fibonacci, and for it is the Tribonacci sequence. Because each entry reads only the last entries, keeping those values in a few variables cuts memory from to . The pattern breaks if order does not matter, as in counting coin combinations, where 1+2 and 2+1 must count once.
Any problem with recurrence is Fibonacci-shaped, so the closed form is where and . This gives computation via matrix exponentiation on , or via iteration, versus for naive recursion.
If you could climb 1, 2, or 3 steps at a time, what would the recurrence be?
Related Problems on PixelBank
A row of houses holds different amounts of cash, and any two neighbouring houses share an alarm, so robbing both triggers it. Which houses give the most money? Greedy rules fail. Grabbing the richest house first loses on [3, 4, 3], where the 4 blocks both 3s, and taking every other house loses on [2, 1, 1, 2], where the best choice is the two end houses.
The previous topic, Climbing Stairs, counted ways and added the cases together. House Robber is the first problem in this chapter that optimises: the recurrence takes a maximum over choices instead of a sum. The state is the best total from the first houses, and the choice at house is binary. Skip it, and the best is whatever the first houses allowed. Rob it, and the previous house must be skipped, so its value adds to the best from the first houses. The adjacency rule is never checked explicitly; it is built into which earlier state each choice reads.
We first derive and trace the recurrence with its constraint. Then we name the include-or-exclude pattern and use it to solve the circular street, where the first and last houses are neighbours.
Definition
House Robber asks for the maximum sum of a subset of non-negative values with no two chosen indices adjacent. With the best total using houses to , . It runs in time and space; the circular variant takes the better of two linear runs.
In this topic
- 1State Transition with Constraints
- 2Include/Exclude Pattern
State Transition with Constraints
Define as the most money obtainable from houses through , where is the cash in house . Every valid plan either skips house or robs it. Skipping leaves the best plan for houses to , which is . Robbing forbids house , so it adds to . Hence , with and . The constraint lives in the index : reading for the rob case could include house and break the rule. Only two earlier values matter, so two variables suffice.
Define loot robbing houses with the adjacency constraint. The recurrence encodes two disjoint cases: (1) skip house so best is , or (2) rob house so best is (we cannot add since it may include house ). The max over a cover of disjoint cases is always valid.
Houses have money [2, 7, 9, 3, 1]. What is the maximum you can rob?
Include/Exclude Pattern
House Robber is one instance of a pattern that recurs throughout DP: walk the items in order and, for each one, either include it, paying whatever constraint that imposes on earlier choices, or exclude it at no cost. The recurrence is the better of the two branches. The same skeleton solves 0/1 knapsack, where including an item uses capacity, and weighted interval scheduling, where including an interval rules out the ones it overlaps. When a constraint wraps around, as with houses on a circle, split the problem into cases that each become linear. If the constraint depends on more history, add a state dimension that records it.
Many DP problems reduce to a binary choice at each index: include element (with constraints on previous state) or exclude it. The state space is then and transitions are , giving time. This pattern generalizes to where encodes the last choice, used in stock-trading problems and subset-sum variants.
How does the house robber differ if the houses are arranged in a circle (first and last are adjacent)?
Related Problems on PixelBank
A delivery robot crosses a warehouse floor divided into cells, each with a traversal cost, and it may move only right or down. Seam carving, which resizes images by deleting a low-energy path of pixels, and sequence alignment in bioinformatics both fill in a two-dimensional table in the same way. In each case the object is a grid, and the answer at one cell depends on the answers at neighbouring cells.
The previous topic, House Robber, indexed its state by one position along a line. A grid needs two indices, so the state becomes , the best answer for reaching cell . Because moves go only right or down, a path enters from the cell above or the cell to the left, and the recurrence picks the better of the two. The table has entries, each computed in constant time. Greedy fails here as well: taking the cheaper neighbour at every step can lead into an expensive region later, which DP avoids by comparing complete path costs.
We first define the 2D state and its boundary cases on a small cost grid. Then we study filling order, the rule that every cell's dependencies must be computed before the cell itself.
Definition
Grid-based DP solves problems on an grid by defining a state for each cell and computing it from neighbouring states. For right and down moves, combines and , with the first row and first column as boundary cases. Filling in row-major order takes time.
In this topic
- 12D State Definition
- 2Filling Order and Dependencies
2D State Definition
Extending DP to a grid means the state needs two coordinates. Let be the minimum cost to reach cell from , including both endpoint costs, where grid[i][j] is the cost of a cell. With only right and down moves, the last step into comes from above or from the left, so . The first row has no cell above and the first column has no cell to the left, so each is a running sum along its edge. Forgetting these boundaries makes code read index , which in Python silently wraps around to the last row or column.
A grid DP state encodes the answer restricted to the subrectangle (or ending at cell ). The state space is with transitions, yielding time and space, which can often be reduced to by keeping only the previous row.
Grid = [[1,3,1],[1,5,1],[4,2,1]]. What is the minimum cost path?
Filling Order and Dependencies
A recurrence is only useful if every value it reads already exists. Draw an arrow from each cell to the cells it depends on: these arrows must form a DAG, and the loops must visit cells in a topological order of it. When reads and , row-major order, top to bottom and left to right, works, and so does column-major order. The direction of the state can also be reversed: define the cost from to the goal and fill from the bottom-right corner. If a cell is read before it is written, the code does not crash; it silently uses the default value, usually zero.
For to be computable, all cells it depends on must already be filled. If the recurrence depends on and , row-major order with both indices increasing works. In general the dependency graph must be a DAG with a topological order compatible with iteration — this is the 2D analog of the 1D rule of filling from base cases outward.
Can you solve grid DP by filling the table from bottom-right to top-left instead?
Related Problems on PixelBank
A robot sits in the top-left cell of an grid and may move only right or down. How many different routes lead to the bottom-right cell? For a 3 by 7 grid there are 28, and for a 10 by 10 grid there are 48,620, far too many to enumerate. Now block some cells, a shelf in a warehouse or a wall on a map, and the question becomes harder to answer with a formula.
The previous topic, Grid-Based DP, took the minimum over the two incoming directions to find the cheapest path. Unique Paths keeps the same grid and the same dependencies, but asks how many paths there are, so the minimum becomes a sum. Every path into arrives from above or from the left, and these groups do not overlap, so their counts add, exactly as in Climbing Stairs. Without obstacles the count also has a closed form, since every path is some arrangement of down moves and right moves.
We first fill the counting table and check it against the binomial coefficient. Then we add obstacles, which break the formula but only require setting blocked cells to zero in the DP.
Definition
Unique Paths counts the monotone lattice paths from to using right and down moves. With the number of paths to , , the edges are 1, and blocked cells are 0. Without obstacles the answer is ; the DP takes time.
In this topic
- 1Counting Paths with DP
- 2Handling Obstacles
Counting Paths with DP
To count routes rather than optimise them, let be the number of distinct right-and-down paths from to . The final move into comes from above or from the left, the two groups share no path, and together they cover every path, so . Every cell in the first row or first column has exactly one path, a straight line, so those entries are 1. The table is Pascal's triangle turned on its side, which explains the closed form . Counts grow quickly, so fixed-width languages need 64-bit integers or a modulus.
The recurrence counts lattice paths from to moving only right or down. A path has exactly steps, down and right, so the closed form is . The DP runs in ; the closed form runs in with big-integer arithmetic.
How many unique paths exist in a 3x3 grid (from top-left to bottom-right)?
Handling Obstacles
Blocked cells break the symmetry that the binomial formula relies on, but the DP barely changes. A blocked cell has no paths into it, so set there; every other cell keeps , and a zero neighbour simply contributes nothing. The subtle case is the edges. The first row is no longer all 1s: once an obstacle appears in it, every cell to its right is unreachable, and the same holds down the first column. If the start or goal is blocked, the answer is 0. Initialising the edges to 1 before applying obstacles is the most common bug.
With obstacles, the recurrence becomes if is blocked, else . The state space stays but the closed-form shortcut is lost because obstacles remove specific lattice paths non-uniformly. This is a canonical example of how local constraints kill closed-form solutions but leave DP untouched.
Grid with obstacle: [[0,0,0],[0,1,0],[0,0,0]] where 1 is obstacle. How many unique paths?
Related Problems on PixelBank
A hiker plans a route across terrain divided into cells, each with a known effort, moving only east or south, and wants the route with the least total effort. The same computation appears in image processing, where seam carving finds the lowest-energy path through the pixels of a picture before removing it.
The previous topic, Unique Paths, counted routes by adding the two incoming counts. Minimum Path Sum asks instead which single route is cheapest, so the addition becomes a minimum, and each cell's own cost is added on arrival. It is the grid-based recurrence from earlier in this chapter applied as a complete interview problem, and it is also a shortest-path problem on a special graph: each cell is a vertex, and edges point only right or down. Because that graph has no cycles, filling the table in row order processes vertices in a topological order, so the DP needs no priority queue and, unlike Dijkstra's algorithm, remains correct even when some costs are negative.
We first build and trace the weighted recurrence. Then we reduce its memory, either by overwriting the input grid or by keeping a single row, and weigh when each is acceptable.
Definition
Minimum Path Sum asks for the smallest total of cell values along a right-and-down path from the top-left to the bottom-right of an grid. With and running sums on the edges, it runs in time, using space with one rolling row.
In this topic
- 1DP on Weighted Grids
- 2In-Place DP Optimization
DP on Weighted Grids
Let be the smallest total of cell values on any right-and-down path from to , counting both ends, where grid[i][j] is the value in that cell. The last move comes from above or from the left, and whichever path reaches that neighbour, the best continuation from there is the same, which is optimal substructure. So , with running sums along the first row and column. Viewed as a graph, the grid is a DAG, and row-major order is a topological order, so each cell is final once computed. This is why negative values, which break Dijkstra, cause no trouble here.
Define cost to reach from . Recurrence: . This is shortest path on a DAG with vertices and edges, so DP is — optimal because any correct algorithm must at least read every cell.
Grid = [[1,2,3],[4,5,6]]. What is the minimum path sum?
In-Place DP Optimization
The full table uses memory, but each cell reads only the cell above and the cell to its left, so less is needed. One option writes each over grid[i][j]: when a cell is processed, its upper and left neighbours already hold finished DP values, and extra space drops to . The cost is that the input is destroyed, which breaks callers that reuse the grid. A safer option keeps a single row of length : before the update, row[j] holds the value from the row above, and row[j-1] already holds the current row's left value. That uses space and leaves the input unchanged.
Since only depends on (the cell above) and (the cell to the left), we can overwrite the input grid in place. This reduces extra space from to while keeping time. The key invariant: when computing , both and are already updated.
Why might in-place modification be undesirable in production code?
Related Problems on PixelBank
A satellite image is converted to a binary map, 1 for buildable land and 0 for water or roads, and a planner needs the largest square plot made entirely of 1s. Checking every square directly, choosing a corner and size and scanning its cells, costs about work, far too slow for a large map.
The previous topic, Minimum Path Sum, stored a cost in each cell. Maximal Square shows that a DP state can store a geometric quantity instead. Let be the side length of the largest all-ones square whose bottom-right corner is at . A square of side ending there must contain three squares of side , ending at the cell above, the cell to the left, and the cell diagonally up-left. The smallest of those three limits how far the new square can grow, which gives a recurrence of three reads and a minimum per cell. One pass solves the map, and the answer is the largest side, squared.
We first set up the state as a side length and trace it on a sample matrix. Then we prove why the minimum of the three neighbours is exactly right, and why the maximum would overclaim.
Definition
Maximal Square asks for the area of the largest square submatrix containing only 1s in an binary matrix. With the side of the largest such square with bottom-right corner at , for a 1-cell and 0 otherwise. The answer is , in time.
In this topic
- 1State as Square Side Length
- 2Why min() of Three Neighbors?
State as Square Side Length
The state does not have to be a cost or a count. Here is the side length of the largest square of 1s whose bottom-right corner is exactly at . Anchoring at one corner makes each square belong to one cell and makes the entry depend only on cells above and to the left, the usual grid filling order. A 0 cell gets , and a 1 in the first row or column gets 1, since no larger square fits. Otherwise the entry comes from the three neighbours. The answer is the largest entry squared: the largest square can end anywhere, not only at the last cell.
Define = side length of the largest all-ones square whose bottom-right corner is at . The answer is . This reparametrization turns a 2D geometric search into a simple 1D scalar DP because a square of side at implies all three smaller squares at have side .
Matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]. What is the maximal square area?
Why min() of Three Neighbors?
Why the minimum? A square with corner contains three squares, ending at , , and . So each neighbour's value must be at least , which gives of the three. The reverse also holds: if all three are at least , their squares together with cell cover a full square. The bound is therefore exact. Using max or a sum instead would claim squares that extend over 0 cells, because one large neighbour says nothing about the rows or columns the other two must cover.
If , then . The captures the bottleneck: the largest square ending at cannot exceed the smallest square meeting at its three neighbors plus one. Using or would create squares that extend into non-existent territory — the is essential to maintain the all-ones invariant.
If dp[i-1][j]=3, dp[i][j-1]=2, dp[i-1][j-1]=3, and matrix[i][j]=1, what is dp[i][j]?