Dynamic programming and memoization
Dynamic programming: solve a problem by solving overlapping subproblems once and remembering the answers. Optimal substructure and overlapping subproblems, memoization (top-down) against tabulation (bottom-up), how to find the state and the recurrence, the running time as the number of states times the work per state, reconstructing the solution, saving space, and the contrast with divide and conquer and greedy. Fibonacci numbers and a grid path as worked examples.
Why it exists
Some recursive definitions are wasteful: the same sub-question is asked again and again. The recursion for the Fibonacci numbers, , makes about calls although only different values exist. Dynamic programming (DP) fixes this by answering every distinct sub-question once and remembering the answer. It turns exponential recursive algorithms into polynomial ones, and it is the standard tool for optimisation problems where a greedy rule fails (0/1 knapsack, longest common subsequence, edit distance, matrix chain multiplication, shortest paths with negative edges). GATE asks both for the method (write the recurrence, fill the table) and for the numbers (a table entry, the number of subproblems, the running time).
Prerequisites
You need recursion and the call stack (recursion and the call stack) and recurrences (solving recurrences).
Precise definitions
Optimal substructure. An optimal solution of the problem contains optimal solutions of its subproblems (so the best answer can be built from best answers of smaller problems).
Overlapping subproblems. The recursion meets the same subproblem many times, so the number of distinct subproblems is small compared with the number of recursive calls. If the subproblems do not overlap the method is divide and conquer (merge sort); if they overlap, remembering the answers pays off.
State. The few numbers that identify a subproblem (a prefix length, a capacity, a pair of indices). The set of states is the table; its size is the number of subproblems.
Recurrence. The rule that gives the answer of a state from answers of smaller states, together with the base cases.
Memoization (top-down). Write the plain recursion, but before computing a state look it up in a table, and after computing it store the result. Only the states the recursion actually reaches are computed.
Tabulation (bottom-up). Fill the table in an order in which every state's dependencies are already filled (small to large), with plain loops and no recursion.
Time. (number of states) (work to compute one state from the smaller ones).
Space. The size of the table (often reducible: see below).
How it works
The recipe
- Define the state and say in words what the table entry means (" is the cheapest path from the start to cell ").
- Write the recurrence for the entry in terms of smaller entries, and the base cases.
- Choose the filling order so that dependencies come first (a topological order of the dependency graph, which must have no cycles).
- Read the answer from the final entry; to recover the solution itself (not only its value) remember which choice gave each entry, or trace back through the table.
- Count states and work per state for the running time.
Fibonacci: the cost of forgetting
The three programs compute by naive recursion, by recursion with a memo table, and by filling a table from the bottom. The program counts calls.
#include <stdio.h>
static long calls;
static long memo[64];
static long fib_naive(int n) {
calls++;
return n < 2 ? n : fib_naive(n - 1) + fib_naive(n - 2);
}
static long fib_memo(int n) {
calls++;
if (n < 2) return n;
if (memo[n] != -1) return memo[n];
return memo[n] = fib_memo(n - 1) + fib_memo(n - 2);
}
static long fib_table(int n, long *steps) {
long a = 0, b = 1;
*steps = 0;
for (int i = 2; i <= n; i++) {
long c = a + b;
a = b;
b = c;
(*steps)++;
}
return n < 2 ? n : b;
}
int main(void) {
int ns[4] = {10, 20, 30, 35};
printf("%3s %10s %12s %11s %12s\n", "n", "F(n)", "naive calls", "memo calls", "table steps");
for (int t = 0; t < 4; t++) {
int n = ns[t];
long steps, v1, v2, v3, c1, c2;
calls = 0;
v1 = fib_naive(n);
c1 = calls;
for (int i = 0; i < 64; i++) memo[i] = -1;
calls = 0;
v2 = fib_memo(n);
c2 = calls;
v3 = fib_table(n, &steps);
printf("%3d %10ld %12ld %11ld %12ld%s\n", n, v1, c1, c2, steps, (v1 == v2 && v2 == v3) ? "" : " MISMATCH");
}
return 0;
} n F(n) naive calls memo calls table steps
10 55 177 19 9
20 6765 21891 39 19
30 832040 2692537 59 29
35 9227465 29860703 69 34Computing Fibonacci numbers three ways: start
Computed by a script, not typed| n | F(n) | naive calls | memoised calls | table additions | naive formula | memo formula |
|---|---|---|---|---|---|---|
| 10 | ||||||
| 20 | ||||||
| 30 | ||||||
| 35 |
Step 0
The same recursion fib(n) = fib(n - 1) + fib(n - 2), with and without remembering answers. The call counts were measured by the C program on this page.
Naive recursion made calls (about for ): the call tree has leaves and repeats , , ... exponentially often, so with . The memoised version made calls: each of the values is computed once (two calls each) and the second call of each pair finds the value ready. The table version did additions with no recursion at all. Both DP versions are in time; the memoised one uses stack depth plus the table, the table version only if only the last two values are kept.
Memoization against tabulation
| Memoization (top-down) | Tabulation (bottom-up) | |
|---|---|---|
| Code | the recursion plus a lookup | loops in dependency order |
| States computed | only those reached | all |
| Overhead | recursion calls, stack depth | none |
| Order of filling | automatic | you must get it right |
| Space saving | hard | easy (keep only the rows needed) |
A grid example, cell by cell
A robot moves from the top left to the bottom right of a grid, one step right or down at a time, paying the cost of each cell it enters (including the first). The cheapest path: let be the cheapest cost to reach cell . The last move into came from above or from the left, so , with the first row and column having only one possible predecessor and . This is optimal substructure: the best path to contains a best path to its predecessor. The same cell is a predecessor of two cells (the cell to its right and the cell below), which is the overlap.
#include <stdio.h>
#define R 4
#define C 5
int main(void) {
int a[R][C] = {{3, 1, 4, 2, 5}, {2, 8, 1, 9, 3}, {6, 2, 3, 1, 4}, {5, 7, 2, 8, 1}}; /* cost of entering each cell */
int dp[R][C]; /* dp[i][j] = cheapest path from (0, 0) to (i, j), moving right or down */
for (int i = 0; i < R; i++) {
for (int j = 0; j < C; j++) {
int best = 0;
if (i > 0 && j > 0) best = dp[i - 1][j] < dp[i][j - 1] ? dp[i - 1][j] : dp[i][j - 1];
else if (i > 0) best = dp[i - 1][j];
else if (j > 0) best = dp[i][j - 1];
dp[i][j] = a[i][j] + best;
}
}
for (int i = 0; i < R; i++) {
for (int j = 0; j < C; j++) printf("%4d", dp[i][j]);
printf("\n");
}
printf("cheapest path from the top left to the bottom right costs %d\n", dp[R - 1][C - 1]);
return 0;
} 3 4 8 10 15
5 12 9 18 18
11 13 12 13 17
16 20 14 21 18
cheapest path from the top left to the bottom right costs 18Step through the filling: the grammar lists the four cases of the recurrence and the one being used is highlighted. Every cell is filled from cells that are already there.
Cheapest path through a grid: start
Computed by a script, not typed- 1dp[0][0] = a[0][0]
- 2dp[0][j] = a[0][j] + dp[0][j-1]
- 3dp[i][0] = a[i][0] + dp[i-1][0]
- 4dp[i][j] = a[i][j] + min(dp[i-1][j], dp[i][j-1])
| dp | col 0 | col 1 | col 2 | col 3 | col 4 |
|---|---|---|---|---|---|
| row 0 | |||||
| row 1 | |||||
| row 2 | |||||
| row 3 |
Step 0
dp[i][j] is the cost of the cheapest path from the top left to cell (i, j) using moves right and down. The cell costs are 3 1 4 2 5 / 2 8 1 9 3 / 6 2 3 1 4 / 5 7 2 8 1. Each cell is filled from cells that were filled before it.
The answer is . To recover the path, start at the end and repeatedly step back to the smaller of the two possible predecessors: the cheapest path goes right, right, down, down, right, right, down through the cells with costs (sum ). There are right-and-down paths in all; the program tries none of them: it does cell computations (each ), so the time is for an grid, instead of the number of paths , which is exponential. (An exhaustive check of all paths confirmed the answer .)
Reducing the space
Each row of the grid table depends only on the row above it. Keeping only two rows (or one, updated in place from left to right) reduces the space from to . The price: the path can no longer be read back from the table (store the choices, or recompute). The same trick works for longest common subsequence and knapsack.
Dynamic programming against divide and conquer and greedy
- Divide and conquer: subproblems are independent, each is solved once anyway; no table is needed.
- Dynamic programming: subproblems overlap; the table makes each one cost once. All choices are tried (the or in the recurrence).
- Greedy: needs the extra greedy-choice property and tries only one choice (greedy method). Where it applies it is faster; where it does not, DP is the safe tool (coin change with ).
Pseudo-polynomial time. Knapsack with capacity takes time, polynomial in the value but exponential in the number of bits of (analysing loops); such algorithms are called pseudo-polynomial.
Worked examples
EX 1How many calls does naive Fibonacci make?
EasyLet be the number of calls of fib_naive(n). and . Then : for , , so (the table shows ). With memoisation, . The ratio grows without bound: for it is more than .
EX 2Count the subproblems
EasyCheapest path in an grid: the state is , so subproblems; each takes work (a comparison and an addition). Time . For longest common subsequence of strings of lengths and (next page): states, each, time .
EX 3Fill the grid table by hand
MediumCosts: first row . of the first row is a running sum: . First column of costs : . Cell with cost : from above , from the left ; the cheaper is , so . Cell with cost : from above , from the left : . This reproduces the animation's second row .
EX 4Is it dynamic programming?
MediumMerge sort: two subproblems that do not overlap, so no table: divide and conquer. Number of ways to climb stairs taking or steps: with , the Fibonacci recurrence: the naive recursion is exponential, the table version is . Shortest path in a graph with negative edges: optimal substructure and overlapping subproblems, so DP (Bellman-Ford), where greedy (Dijkstra) fails.
EX 5Time and space questions
GATE level(a) A DP table of size where each entry takes from its neighbours: time , space , reducible to if only the value is needed. (b) Memoised Fibonacci: time , stack depth . (c) A recurrence without memoisation: . (d) Why is knapsack DP not a polynomial-time algorithm? Its time grows with the value of , which is exponential in the number of bits of .
Cheat box
Cheat box
- DP = optimal substructure + overlapping subproblems; solve each distinct subproblem once.
- Recipe: state, recurrence with base cases, filling order, answer, (trace back), count states work.
- Memoization: recursion + table lookup (only reached states). Tabulation: loops in dependency order (all states, no recursion, easy space saving).
- Time = states work per state. Fibonacci: naive , memo/table ( calls memoised, naive).
- Grid path: , .
- Keep only the last row to save space (value only).
- Greedy: one choice, needs the greedy-choice property. DP: all choices. D&C: no overlap.
- Knapsack is pseudo-polynomial.
Traps and GATE patterns
Connections
Unlocks
Across subjects
DP contrasts with divide and conquer (no overlap) and with the greedy method (one choice). Its standard applications are in LCS, edit distance, knapsack and matrix chain and LIS and coin change; the shortest-path DP of Floyd-Warshall and Bellman-Ford follow the same pattern. The recurrences are those of solving recurrences; the stack depth of memoisation is that of recursion.
PYQs
4 previous year questions from your book. I re-solved each one myself (1 by running or simulating, 3 by reasoning only); the book's key is shown next to my answer. All PYQs
Consider a table T, where the elements T[i][j],
0 <= i, j <= n, represent the cost of the optimal
solutions of different subproblems of a problem
that is being solved using a dynamic
programming algorithm. The recursive
formulation to compute the table entries is as
follows:
T[0][k] = T[k][0] = 1
for k = 0, 1, 2, ..., n
T[i][j] = 2T[i-1][j]+3T[i][j-1] for 1 ≤ i, j ≤ n
Consider the following two algorithms to
compute entries of T. Assume that for both the
algorithms, for all 0 ≤ i, j ≤ n, T[i][j] has been
initialized to 1.
Algorithm B1:
For i = 1 to n
For j = 1 to n
T[i][j] = 2T[i - 1][j] + 3T[i][j - 1]
Algorithm B2:
For s = 2 to 2n
For i = 1 to n
For j = 1 to n
If (i + j == s)
T[i][j] = 2T[i - 1][j] + 3T[i][j - 1]
Algorithm Bk, k ∈ {1, 2}, is said to be correct if
and only if it calculates the correct values of
T[i][j], for all 0 ≤ i, j ≤ n, as per the recursive
formulation, at the end of the execution of the
algorithm Bk.
Which one of the following statements is true?- (a)Both algorithms B1 and B2 are correct
- (b)Algorithm B1 is correct, but algorithm B2 is incorrect
- (c)Algorithm B2 is correct, but algorithm B1 is incorrect
- (d)Both algorithms B1 and B2 are incorrect
Consider the following table: Algorithms Design Paradigms P Kruskal i Divide and Conquer Q Quicksort ii Greedy R Floyd- Warshall iii Dynamic Programming
- (a)P-(ii), Q-(iii), R-(i)
- (b)P-(iii), Q-(i), R-(ii)
- (c)P-(ii), Q-(i), R-(iii)
- (d)P-(i), Q-(ii), R-(iii)
Given below are some algorithms, and some algorithm design paradigms. List - I P. Dijkstra's Shortest Path Q. Floyd-Warshall algorithm to compute all pairs shortest path R. Binary search on a sorted array S. Backtracking search on a graph List – II i. Divide and Conquer ii. Dynamic Programming iii. Greedy design iv. Depth-first search v. Breadth-first search Match the above algorithms on the left to the corresponding design paradigm they follow.
- (a)P-i, Q-iii, R-i, S-v.
- (b)P-iii, Q-iii, R-i, S-v.
- (c)P-iii, Q-ii, R-i, S-iv.
- (d)P-iii, Q-ii, R-i, S-v.
Match the following: (P) Prim's algorithm for minimum spanning tree (i) Backtracking (Q) Floyd-Warshall algorithm for all pairs shortest paths (ii) Greedy method (R) Mergesort (iii) Dynamic programming (S) Hamiltonian circuit (iv) Divide and conquer
- (a)P-iii, Q-ii, R-iv, S-i
- (b)P-i, Q-ii, R-iv, S-iii
- (c)P-ii, Q-iii, R-iv, S-i
- (d)P-ii, Q-i, R-iii, S-iv
Self-check
Question 1
How many distinct subproblems does the recursion for have, and how many calls does the naive version make?
Show answerHide
distinct values ; the naive version makes calls.
Question 2
What is the time and space of the grid path DP on an grid if only the cost (not the path) is needed?
Show answerHide
Time ; space with one row kept (or with the full table).
Question 3
Difference between memoization and tabulation?
Show answerHide
Memoization is the recursion plus a lookup table and computes only the states that are reached; tabulation fills all states bottom-up with loops in dependency order.
Question 4
When is dynamic programming not needed even though the problem is recursive?
Show answerHide
When the subproblems do not overlap (merge sort, binary search): plain divide and conquer already solves each subproblem once.
Revision card
One-screen revision
- Optimal substructure + overlapping subproblems; state, recurrence, base case, order, answer.
- Memoization (top-down, reached states) / tabulation (bottom-up, all states, space saving).
- Time = #states work per state. Fibonacci: .
- Grid path ; keep one row for the value only.
- Greedy: one choice; DP: all choices; D&C: no overlap. Knapsack is pseudo-polynomial.
References
- ·Cormen, Leiserson, Rivest, Stein, Introduction to Algorithms (3rd edition), Chapter 15 Dynamic Programming (15 intro, 15.3 elements of dynamic programming)
- ·Kleinberg and Tardos, Algorithm Design, Chapter 6 Dynamic Programming (6.1 and 6.2)
- ·Dasgupta, Papadimitriou, Vazirani, Algorithms, Chapter 6 Dynamic programming
Verification
The call counts of the Fibonacci program (naive 2F(n+1) - 1, memoised 2n - 1, table n - 1) were recomputed by independent recursion and match for n = 10, 20, 30, 35. The grid table was compared with the best of ALL right-and-down paths (35 paths for the page grid, and 100 random grids of up to 4 by 4); the step-by-step animation is the page program with a print after each assignment and its final table equals the table printed by the page program. Textbook chapter references are from memory. SECOND PASS (2026-11-02, done by the assistant, not a human check): every Self-check question was solved before the printed answer was read and all agreed; the definitions, worked examples, cheat box and traps were read claim by claim and the numbers re-derived by hand or by a small script (loop counts, recurrences, merge and quicksort counts and averages, hash probes and load-factor formulas, DP tables, MST and shortest-path runs, Floyd matrices); long C programs were not re-read line by line (they are compiled and compared by npm run verify:c, and their printed outputs were checked against the prose); tables and animations are machine output and were read, not re-run; No other defect found. Status raised to self-checked; verified needs a textbook or human check.
last verified: never
My notes (saved in this browser)