Data structures and algorithms path · Searching and reusing work · Topic 12 of 13

Memoisation and dynamic programming

Not started

Dynamic programming is a way of solving a problem whose answer can be built from answers to smaller versions of the same problem, when those smaller versions repeat. Instead of recomputing them, you compute each one once and store it. Done lazily inside a recursive function, that is memoisation; done eagerly by filling a table from the bottom up, it is tabulation. Either way, an algorithm that would take exponential time often becomes quadratic or linear.

Why it matters: It turns exponential brute force into polynomial time for diffs, spell checkers, route costs and scheduling, and memoisation is the same idea as every cache you run.

  1. 1Overlapping subproblems

    If a recursive solution asks the same question many times, caching the answers removes the repeats.

  2. 2Memoisation is top-down

    Keep the natural recursion and store each result in a map keyed by its arguments. Compute on first request, look up after that.

  3. 3Tabulation is bottom-up

    Fill a table from the smallest subproblems upwards, in an order where everything a cell needs is already filled.

  4. 4Often only the last row matters

    Many tables only look one row back, so you can keep two rows instead of the whole table.

1

How it works

The explanation, with examples you can run.

Memoisation: cache the recursion

The textbook example is Fibonacci, where fib(n) = fib(n − 1) + fib(n − 2). Written directly, fib(30) recomputes fib(28) twice, fib(27) three times and so on, so the number of calls grows exponentially. A cache keyed by n fixes it without changing the shape of the code:

⌘/Ctrl + Enter
OutputNot run yet

Press Run to see what your code prints, in order.

Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.

From 2.7 million calls to 31: one per distinct subproblem. Python has this built in as the @functools.cache decorator (and @lru_cache(maxsize=…) for a bounded version). Memoisation is only correct for pure functions, ones whose result depends only on their arguments. It is also the same idea as an application cache in front of a database, with the same hard question: when does a stored answer stop being true?

Tabulation: fill a table

Memoised recursion still uses the call stack, and its depth can be a problem. Tabulation computes subproblems in an order where each one's inputs are already known, usually with plain loops. Edit distance, the minimum number of single-character inserts, deletes and substitutions to turn one string into another, is a good real example: it powers spell-check suggestions, fuzzy search and parts of diff tools.

Let d[i][j] be the distance between the first i characters of a and the first j of b. Turning nothing into j characters takes j inserts, so the first row is 0, 1, 2, …. Every other cell is the cheapest of three moves: delete from a (d[i−1][j] + 1), insert into a (d[i][j−1] + 1), or substitute (d[i−1][j−1], plus 1 if the characters differ). Each row only needs the row above it, so two rows are enough:

⌘/Ctrl + Enter
OutputNot run yet

Press Run to see what your code prints, in order.

Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.

"kitten" to "sitting" is 3: substitute k→s, substitute e→i, insert g. The table has O(m × n) cells, so the time is O(m × n); keeping two rows makes the memory O(n).

Recognising a DP problem

Look for two properties. Optimal substructure: the best answer for the whole is built from the best answers for parts (the shortest route to C through B contains the shortest route to B). Overlapping subproblems: the same parts come up again and again. Typical shapes are prefixes of one or two sequences (edit distance, longest common subsequence, the problem behind diff), capacities (knapsack, making change with the fewest coins), and positions in a grid. Write the recurrence first, check how many distinct states it has, and that number times the work per state is your running time.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.Naive recursive fib(30) makes about 2.7 million calls. How many does the memoised version make?
  2. 2.When is memoisation unsafe?
  3. 3.The edit distance table for strings of lengths m and n has (m + 1) × (n + 1) cells. Why can the implementation keep only two rows?

0 of 3 right so far. Get all three to finish this topic.