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.
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:
There are two main approaches to implementing DP:
This chapter covers:
Click any topic to jump in
Overlapping subproblems, optimal substructure, memoization vs tabulation.
Each tackles a different aspect
The classic 1D recurrence: $dp[i] = dp[i-1] + dp[i-2]$, Fibonacci in disguise.
Include/exclude pattern with adjacency constraint: $dp[i] = \max(dp[i-1], dp[i-2] + a_i)$.
2D state design and row-by-row filling order with clear dependencies.
Each tackles a different aspect
Counting lattice paths: $dp[i][j] = dp[i-1][j] + dp[i][j-1]$ and its binomial closed form.
Weighted grid DP with in-place $O(1)$ extra space optimization.
State as side length with the $\min$-of-three-neighbors recurrence.
Dynamic programming transforms exponential-time recursive solutions into polynomial-time algorithms by eliminating redundant computation. The key insight is that if you have already solved a subproblem, you should never solve it again---just look up the answer.
Memoization (top-down) keeps the recursive structure but adds a cache. Tabulation (bottom-up) eliminates recursion entirely by filling a table in the right order. Both give the same time complexity, but tabulation avoids recursion stack overhead while memoization only solves subproblems that are actually needed.
A problem has overlapping subproblems when solving it requires solving the same smaller instances multiple times. The classic example is Fibonacci: computing fib(5) requires fib(4) and fib(3), but fib(4) itself requires fib(3) and fib(2). Without caching, fib(3) is computed twice, fib(2) three times, and so on. This redundancy causes naive recursion to run in time.
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)?
A problem has optimal substructure if the optimal solution contains optimal solutions to its subproblems. For example, the shortest path from A to C through B must use the shortest path from A to B and the shortest path from B to C. If either sub-path were suboptimal, we could improve the overall path---a contradiction.
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 (top-down) uses recursion plus a cache (dictionary or array). You write the recursive solution naturally, then add if subproblem in memo: return memo[subproblem] at the top. Tabulation (bottom-up) creates a table, fills in base cases, then iterates through subproblems in dependency order. Tabulation avoids recursion depth limits and often has better cache locality.
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?