A handful of ideas. Two examples. Many drawings.
Dynamic programming is what you do when a recursion repeats itself. Instead of solving the same subproblem ten thousand times, you solve it once, write the answer down, and look it up next time it appears.
That's the entire trick. The hard part is just two things: noticing that subproblems overlap, and figuring out the recurrence that ties them together. Everything else — top-down memoization, bottom-up tables, optimizing space — is implementation detail.
The same smaller question gets asked over and over. Without memory, you re-derive its answer every time.
The answer to a problem is built from the answers to smaller versions of itself. A clean recurrence exists.
Compute each subproblem once. Cache it (memoize) or fill a table bottom-up (tabulate). Done.
The two examples below are the cleanest illustrations of these ideas. The first shows you the waste that DP eliminates. The second shows you the table that DP fills.
F(n) = F(n−1) + F(n−2), with F(0) = 0 and F(1) = 1. The most innocent recursion in computer science — and a complete disaster if you actually run it that way.
Below is the call tree for F(5). Same color = same value. Notice how often the colors repeat. Each repeat is a subproblem you computed from scratch when you didn't have to.
Once you see the duplicates, the patch is obvious: write each answer down the first time. Even cleaner — build the table from the bottom up, so you never need to recurse at all.
function fib(n) { const dp = [0, 1]; for (let i = 2; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n]; }
n grows. F(50) goes from ~25 billion calls down to 51.Stand in the top-left of a grid. You may step right or down only. How many distinct paths reach the bottom-right corner?
The recurrence almost writes itself. To reach any cell, you must have arrived from the cell directly above it or directly to its left. So the number of paths to a cell is the sum of paths to those two predecessors.
Edge cells — the top row and left column — have exactly one path to them, straight along the edge. Those are your base cases. Now fill the table, row by row.
function uniquePaths(rows, cols) { const dp = Array.from({length: rows}, () => Array(cols).fill(1)); for (let i = 1; i < rows; i++) { for (let j = 1; j < cols; j++) { dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; } } return dp[rows - 1][cols - 1]; }