G27
Self-checkedTier 1 13 min readalgo.dp-memoization

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, F(n)=F(n−1)+F(n−2)F(n) = F(n-1) + F(n-2), makes about 1.6n1.6^n calls although only n+1n + 1 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) ×\times (work to compute one state from the smaller ones).

Space. The size of the table (often reducible: see below).

How it works

The recipe

  1. Define the state and say in words what the table entry means ("dp[i][j]\text{dp}[i][j] is the cheapest path from the start to cell (i,j)(i, j)").
  2. Write the recurrence for the entry in terms of smaller entries, and the base cases.
  3. Choose the filling order so that dependencies come first (a topological order of the dependency graph, which must have no cycles).
  4. 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.
  5. Count states and work per state for the running time.

Fibonacci: the cost of forgetting

The three programs compute F(n)F(n) 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           34

Computing Fibonacci numbers three ways: start

Computed by a script, not typed
nF(n)naive callsmemoised callstable additionsnaive formulamemo 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.

0 / 4
computed by script: the quantities are measured by running C, or evaluated, one row at a time. python3 scripts/gen-algo-dp-traces.py

Naive recursion made 2F(n+1)−12F(n+1) - 1 calls (about 3⋅1073 \cdot 10^7 for n=35n = 35): the call tree has F(n+1)F(n+1) leaves and repeats F(n−2)F(n-2), F(n−3)F(n-3), ... exponentially often, so T(n)=T(n−1)+T(n−2)+1=Θ(φn)T(n) = T(n-1) + T(n-2) + 1 = \Theta(\varphi^n) with φ=1.618\varphi = 1.618. The memoised version made 2n−12n - 1 calls: each of the n−1n - 1 values F(2),…,F(n)F(2), \ldots, F(n) is computed once (two calls each) and the second call of each pair finds the value ready. The table version did n−1n - 1 additions with no recursion at all. Both DP versions are Θ(n)\Theta(n) in time; the memoised one uses Θ(n)\Theta(n) stack depth plus the table, the table version only O(1)O(1) if only the last two values are kept.

Memoization against tabulation

Memoization (top-down)Tabulation (bottom-up)
Codethe recursion plus a lookuploops in dependency order
States computedonly those reachedall
Overheadrecursion calls, stack depthnone
Order of fillingautomaticyou must get it right
Space savinghardeasy (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 dp[i][j]\text{dp}[i][j] be the cheapest cost to reach cell (i,j)(i, j). The last move into (i,j)(i, j) came from above or from the left, so dp[i][j]=a[i][j]+min⁡(dp[i−1][j],dp[i][j−1])\text{dp}[i][j] = a[i][j] + \min(\text{dp}[i-1][j], \text{dp}[i][j-1]), with the first row and column having only one possible predecessor and dp[0][0]=a[0][0]\text{dp}[0][0] = a[0][0]. This is optimal substructure: the best path to (i,j)(i, j) 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 18

Step 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
  1. 1dp[0][0] = a[0][0]
  2. 2dp[0][j] = a[0][j] + dp[0][j-1]
  3. 3dp[i][0] = a[i][0] + dp[i-1][0]
  4. 4dp[i][j] = a[i][j] + min(dp[i-1][j], dp[i][j-1])
dpcol 0col 1col 2col 3col 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.

0 / 20
computed by script: the quantities are measured by running C, or evaluated, one row at a time. python3 scripts/gen-algo-dp-traces.py

The answer is dp[3][4]=18\text{dp}[3][4] = 18. 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 3,1,4,1,3,1,4,13, 1, 4, 1, 3, 1, 4, 1 (sum 1818). There are (73)=35\binom{7}{3} = 35 right-and-down paths in all; the program tries none of them: it does 4×5=204 \times 5 = 20 cell computations (each Θ(1)\Theta(1)), so the time is Θ(RC)\Theta(RC) for an R×CR \times C grid, instead of the number of paths (R+C−2R−1)\binom{R+C-2}{R-1}, which is exponential. (An exhaustive check of all 3535 paths confirmed the answer 1818.)

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 Θ(RC)\Theta(RC) to Θ(C)\Theta(C). 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 min⁡\min or max⁡\max 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 {1,3,4}\{1, 3, 4\}).

Pseudo-polynomial time. Knapsack with capacity WW takes Θ(nW)\Theta(nW) time, polynomial in the value WW but exponential in the number of bits of WW (analysing loops); such algorithms are called pseudo-polynomial.

Worked examples

EX 1How many calls does naive Fibonacci make?

Easy

Let c(n)c(n) be the number of calls of fib_naive(n). c(0)=c(1)=1c(0) = c(1) = 1 and c(n)=1+c(n−1)+c(n−2)c(n) = 1 + c(n-1) + c(n-2). Then c(n)=2F(n+1)−1c(n) = 2F(n+1) - 1: for n=10n = 10, F(11)=89F(11) = 89, so c=177c = 177 (the table shows 177177). With memoisation, 2n−1=192n - 1 = 19. The ratio grows without bound: for n=35n = 35 it is more than 400 000400\,000.

EX 2Count the subproblems

Easy

Cheapest path in an R×CR \times C grid: the state is (i,j)(i, j), so RCRC subproblems; each takes O(1)O(1) work (a comparison and an addition). Time Θ(RC)\Theta(RC). For longest common subsequence of strings of lengths mm and nn (next page): (m+1)(n+1)(m+1)(n+1) states, O(1)O(1) each, time Θ(mn)\Theta(mn).

EX 3Fill the grid table by hand

Medium

Costs: first row 3,1,4,2,53, 1, 4, 2, 5. dp\text{dp} of the first row is a running sum: 3,4,8,10,153, 4, 8, 10, 15. First column of costs 3,2,6,53, 2, 6, 5: 3,5,11,163, 5, 11, 16. Cell (1,1)(1, 1) with cost 88: from above 44, from the left 55; the cheaper is 44, so 8+4=128 + 4 = 12. Cell (1,2)(1, 2) with cost 11: from above 88, from the left 1212: 1+8=91 + 8 = 9. This reproduces the animation's second row 5,12,9,18,185, 12, 9, 18, 18.

EX 4Is it dynamic programming?

Medium

Merge sort: two subproblems that do not overlap, so no table: divide and conquer. Number of ways to climb nn stairs taking 11 or 22 steps: W(n)=W(n−1)+W(n−2)W(n) = W(n-1) + W(n-2) with W(0)=W(1)=1W(0) = W(1) = 1, the Fibonacci recurrence: the naive recursion is exponential, the table version is Θ(n)\Theta(n). 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 m×nm \times n where each entry takes O(1)O(1) from its neighbours: time Θ(mn)\Theta(mn), space Θ(mn)\Theta(mn), reducible to Θ(min⁡(m,n))\Theta(\min(m, n)) if only the value is needed. (b) Memoised Fibonacci: time Θ(n)\Theta(n), stack depth Θ(n)\Theta(n). (c) A recurrence T(n)=T(n−1)+T(n−2)+1T(n) = T(n-1) + T(n-2) + 1 without memoisation: Θ(φn)\Theta(\varphi^n). (d) Why is knapsack DP not a polynomial-time algorithm? Its time Θ(nW)\Theta(nW) grows with the value of WW, which is exponential in the number of bits of WW.

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 ×\times work.
  • Memoization: recursion + table lookup (only reached states). Tabulation: loops in dependency order (all states, no recursion, easy space saving).
  • Time = states ×\times work per state. Fibonacci: naive Θ(φn)\Theta(\varphi^n), memo/table Θ(n)\Theta(n) (2n−12n - 1 calls memoised, 2F(n+1)−12F(n+1) - 1 naive).
  • Grid path: dp[i][j]=a[i][j]+min⁡(dp[i−1][j],dp[i][j−1])\text{dp}[i][j] = a[i][j] + \min(\text{dp}[i-1][j], \text{dp}[i][j-1]), Θ(RC)\Theta(RC).
  • 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 Θ(nW)\Theta(nW) is pseudo-polynomial.

Traps and GATE patterns

Connections

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

GATE 2026 · Set 2 · MCQ · 2 marksQ1 · page 130 of the book
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
GATE 2017 · Set 1 · MCQ · 2 marksQ4 · page 146 of the book
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)
GATE 2015 · Set 2 · MCQ · 1 markQ6 · page 147 of the book
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.
GATE 2015 · Set 1 · MCQQ7 · page 147 of the book
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 F(30)F(30) have, and how many calls does the naive version make?

Show answer

3131 distinct values F(0),…,F(30)F(0), \ldots, F(30); the naive version makes 2F(31)−1=2 692 5372F(31) - 1 = 2\,692\,537 calls.

Question 2

What is the time and space of the grid path DP on an R×CR \times C grid if only the cost (not the path) is needed?

Show answer

Time Θ(RC)\Theta(RC); space Θ(C)\Theta(C) with one row kept (or Θ(RC)\Theta(RC) with the full table).

Question 3

Difference between memoization and tabulation?

Show answer

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 answer

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 ×\times work per state. Fibonacci: Θ(φn)→Θ(n)\Theta(\varphi^n) \to \Theta(n).
  • Grid path Θ(RC)\Theta(RC); keep one row for the value only.
  • Greedy: one choice; DP: all choices; D&C: no overlap. Knapsack Θ(nW)\Theta(nW) 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