In Tree recursion (4.3), fib(6) took 25 calls to compute the number 8. Most of those calls were repeats: fib(2) was worked out five separate times, from scratch, and gave 1 every time. A person doing this by hand would write the answer down the first time and look it up after. A program can do the same.
Remembering answers instead of recomputing
The calls of fib(6), with and without a table
#define MAXN 93 static long calls = 0;static long long memo[MAXN]; long long fib_memo(int n){ calls++; if (n < 2) return n; if (memo[n] != -1) return memo[n]; long long a = fib_memo(n - 1); long long b = fib_memo(n - 2); memo[n] = a + b; return memo[n];} long long fib_loop(int n){ if (n == 0) return 0; long long prev = 0, cur = 1; for (int i = 2; i <= n; i++) { long long next = prev + cur; prev = cur; cur = next; } return cur;} int main(void){ for (int i = 0; i < MAXN; i++) memo[i] = -1; long long f = fib_memo(6); printf("memoised fib(6) = %lld in %ld calls\n", f, calls); long long g = fib_loop(6); printf("loop fib(6) = %lld\n", g); return 0;}Without a table, fib(6) makes 25 calls: fib(4) twice, fib(3) three times, fib(2) five times. First, every cell of memo is set to −1.
- calls without a table
- 25
- with it
- 0
- frames of fib_memo
- 0
Output
Describe this picture
Lines 3 to 43 of memo.c, with the line about to run marked, beside a picture in two parts. At the top is a tree of calls: one circle per call, the n of the call inside it, the value it returned under it once done. Under the tree is a row of seven cells, “long long memo[93]: cells 0 to 6”, with indices 0 to 6 above them. The clip plays once, about 25 seconds. It opens on the tree of plain fib(6) from Tree recursion, all 25 calls, under the words “Without a table: 25 calls; repeats share a colour”: the two calls of fib(4) are filled in one colour, the three of fib(3) in a second and the five of fib(2) in a third. The cells read 0; main’s loop then sets each one to −1. Then the words become “With the table: fib_memo(6)”, every circle dims, and the calls of fib_memo light up in their places in the same tree, edged in the accent colour while they are on the stack: down the left edge from 6 to 1, then 0. On the way back, cell 2 becomes 1, then cell 3 becomes 2. The second call of fib(2), under fib(4), finds 1 in cell 2, which lights: its circle turns dashed and the two calls under it disappear from the tree. Cell 4 becomes 3. The second call of fib(3), under fib(5), finds 2 in cell 3, and the four calls under it disappear. Cell 5 becomes 5. The second call of fib(4), under fib(6), finds 3 in cell 4, and the eight calls under it disappear. Cell 6 becomes 8. Eleven circles are left: a path from 6 down to 1, with one short branch off each. Then fib_loop runs through its rounds, and the picture stays. Under the picture are one sentence on the current step and three readouts: “calls without a table”, 25 throughout; “with it”, which counts up to 11; and “frames of fib_memo”, which reaches 6 at the deepest. The output is two lines: memoised fib(6) = 8 in 11 calls and loop fib(6) = 8. The transport plays and pauses, steps back or forward, and scrubs; a paused step is kept in the page address as memoisation.step.
Repeated work
Count the calls of plain fib(6) by what they ask for: fib(5) once, fib(4) twice, fib(3) three times, fib(2) five times, fib(1) eight times and fib(0) five times. That is 1 + 1 + 2 + 3 + 5 + 8 + 5 = 25. The same small questions are asked again and again, and every repeat redoes the whole subtree under it. These are repeated subproblems, and they are why the calls grow exponentially.
The table
Memoisation means storing each answer the first time it is computed, and looking it up every time after. For fib the answers are indexed by n, so the table is an array, memo, with one cell per n.
long long fib_memo(int n)
{
calls++;
if (n < 2)
return n;
if (memo[n] != -1)
return memo[n];
long long a = fib_memo(n - 1);
long long b = fib_memo(n - 2);
memo[n] = a + b;
return memo[n];
}
The only changes from plain fib are the two lines that read the table and the line that writes it. Before the first call, main sets every cell to −1, which means “not worked out yet”. Why not 0? Because 0 is a real answer: fib(0) = 0. A cell holding 0 would look empty, and that answer would be worked out again every time it was asked for.
The table is 93 cells long because fib(92) = 7,540,113,804,746,346,429 is the largest Fibonacci number that fits in a long long; fib(93) does not.
Key idea
Memoisation stores the answer to each subproblem in a table the first time it is computed, and looks it up after. The answers do not change; only the repeated work goes.
What the table does to the calls
Watch the tree in the clip. The first call of each fib(k) runs in full and stores its answer. Every later call of fib(k) finds the answer in the table at once, and the whole subtree that plain fib would have built under it is never made. What is left is a path down the left of the tree, from 6 to 1, with one short branch off each node: 11 calls instead of 25.
In general, only the first calls of fib(n), fib(n − 1), … down to fib(2) make further calls, two each; every other call stops at once, either because it is a base case or because it finds its answer. That is n − 1 calls making 2(n − 1) more, plus the first: memoised fib(n) makes 2n − 1 calls for n ≥ 1: 11 for fib(6), 59 for fib(30).
What the table does not change is the depth. The first call of fib(6) still has to go all the way down to fib(1) before anything is stored, so the stack still holds 6 frames of fib_memo at the deepest, just as plain fib did. The table saves calls, not stack.
Top down and bottom up
fib_memo starts at the top, fib(6), and fills the table as the answers come back: top down. But we know the order the cells get filled: 2, 3, 4, 5, 6. So we can fill them in that order directly, with a loop and no recursion at all: bottom up. This is the idea behind dynamic programming.
For fib, each cell needs only the two cells before it, so the loop does not even need the table. Two variables are enough:
long long fib_loop(int n)
{
if (n == 0)
return 0;
long long prev = 0, cur = 1;
for (int i = 2; i <= n; i++) {
long long next = prev + cur;
prev = cur;
cur = next;
}
return cur;
}
Before each round, cur is fib(i − 1) and prev is fib(i − 2). For n = 6 the loop runs 5 times and ends with cur = 8.
The costs
| Version | Calls or rounds for fib(n) | Extra memory |
|---|---|---|
| plain recursion (4.3) | grows exponentially: 25 for fib(6) | the stack, n frames |
| memoised | 2n − 1 calls, in proportion to n | the table, plus the stack, n frames |
| loop | n − 1 rounds, in proportion to n | two variables, the same for any n |
Memoisation trades memory for time: a table of n cells buys a run that grows in proportion to n instead of exponentially. The loop then removes the stack as well.
Running it yourself
gcc -std=c11 -Wall -Wextra -pedantic memo.c -o memo
./memo
It prints memoised fib(6) = 8 in 11 calls and loop fib(6) = 8.
Worked example
fib(30), three ways. Plain recursion makes 2,692,537 calls (Tree recursion, 4.3). The memoised version, with 30 in place of 6, makes 59, and the loop runs 29 rounds. All three give 832,040.
Bottom up by hand to fib(8). Start with (prev, cur) = (0, 1), and at each round replace them with (cur, prev + cur):
(0, 1), (1, 1), (1, 2), (2, 3), (3, 5), (5, 8), (8, 13), (13, 21).
That is seven rounds, for i = 2 to 8, and cur ends at 21, so fib(8) = 21.
Why −1, once more. Suppose the table started at 0 and a function stored an answer that is itself 0. Every later call would read 0, take it for “empty” and do the work again. fib escapes only because a call with n < 2 returns before it reads the table, so fib(0) is never stored. A marker that can never be a real answer, like −1 for a count, is the safe choice.
Before you read on, three to try:
- How many calls does memoised fib(10) make? (19.)
- After
fib_memo(6), which cells of memo hold an answer? (2 to 6; cells 0 and 1 still hold −1.) - If
maincalledfib_memo(6)a second time straight after, how many calls would that make? (1: the answer is already in memo[6].)
Where it’s used
Dynamic programming. Many problems split into smaller ones that repeat, and a table of their answers makes them fast. Edit distance, the fewest single-letter changes that turn one word into another, is filled in a table this way; spelling suggestions and the comparison of DNA sequences use it. So does finding shortest paths in a graph (19.5), and choosing the cheapest order in which to multiply a chain of matrices. Binomial coefficients (4.8), next, are another.
Caching. Any function that always gives the same answer for the same input, and takes a long time, can keep a table of past answers: a web server keeps pages it has already built, a compiler keeps results it has already worked out.
Reference card
| Idea | In this lesson |
|---|---|
| repeated subproblems | the same fib(k) asked for many times: fib(2) five times in fib(6) |
| memoisation | store each answer the first time; look it up after |
| empty marker | −1, because 0 is a real answer |
| top down | recursion, filling the table as answers return |
| bottom up | a loop from the smallest n upwards: dynamic programming |
| calls for fib(n) | plain: exponential; memoised: 2n − 1; loop: n − 1 rounds |
| depth | memoised fib is still n frames deep; the loop uses one frame |
| fib(6), fib(30) | 25 → 11 calls; 2,692,537 → 59 calls |
Platform: C11, gcc on x86-64 Linux, long long 8 bytes.