Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Coding · Dynamic programming

Top-down and bottom-up

Lesson · 11 min read · Dynamic programming · Order of computation

Compute the same recurrence either by memoised recursion or by filling a table in dependency order. How to choose, and how to save memory.

Two ways to evaluate one recurrence

Top-down (memoisation) keeps the recursive function and adds a cache: before computing a state, look it up; after computing it, store it. The recursion visits states in whatever order the calls happen to need them.

from functools import lru_cache

def grid_paths(rows, cols):
    @lru_cache(maxsize=None)
    def paths(r, c):
        if r == 0 or c == 0:
            return 1
        return paths(r - 1, c) + paths(r, c - 1)
    return paths(rows - 1, cols - 1)

Bottom-up (tabulation) drops the recursion and fills a table in an order where every state's dependencies are already filled. For paths(r, c) that order is row by row, left to right.

def grid_paths(rows, cols):
    t = [[1] * cols for _ in range(rows)]      # row 0 and column 0 are base cases
    for r in range(1, rows):
        for c in range(1, cols):
            t[r][c] = t[r - 1][c] + t[r][c - 1]
    return t[rows - 1][cols - 1]

Bottom-up for a 4 × 5 grid: every cell reads two cells that are already filled.

  1. Step 1 / 13
    01234011111112131

    Base cases filled first.

  2. Step 2 / 13
    012340111111122131

    (1, 1) = 1 from above + 1 from the left = 2.

  3. Step 3 / 13
    0123401111111232131

    (1, 2) = 1 from above + 2 from the left = 3.

  4. Step 4 / 13
    01234011111112342131

    (1, 3) = 1 from above + 3 from the left = 4.

  5. Step 5 / 13
    012340111111123452131

    (1, 4) = 1 from above + 4 from the left = 5.

  6. Step 6 / 13
    0123401111111234521331

    (2, 1) = 2 from above + 1 from the left = 3.

  7. Step 7 / 13
    01234011111112345213631

    (2, 2) = 3 from above + 3 from the left = 6.

  8. Step 8 / 13
    0123401111111234521361031

    (2, 3) = 4 from above + 6 from the left = 10.

  9. Step 9 / 13
    012340111111123452136101531

    (2, 4) = 5 from above + 10 from the left = 15.

  10. Step 10 / 13
    0123401111111234521361015314

    (3, 1) = 3 from above + 1 from the left = 4.

  11. Step 11 / 13
    012340111111123452136101531410

    (3, 2) = 6 from above + 4 from the left = 10.

  12. Step 12 / 13
    01234011111112345213610153141020

    (3, 3) = 10 from above + 10 from the left = 20.

  13. Step 13 / 13
    0123401111111234521361015314102035

    (3, 4) = 15 from above + 20 from the left = 35. The answer, 35, is in the bottom-right cell.

Choosing the order

A valid order is a topological order of the subproblem graph: any order in which each state comes after the states it reads. In practice it is one of a few shapes:

  • 1-D, reading smaller indices: left to right.
  • Prefixes of two sequences: row by row, each row left to right.
  • Intervals [i, j]: by increasing length, because an interval reads shorter ones.
  • Knapsack (item, capacity): item by item; within an item, the capacity order depends on whether the item may be reused (see the knapsack modules).

Which to use

A good habit in interviews: write top-down first to get the recurrence right, then convert to bottom-up if the interviewer asks for speed, iteration or less memory.

Top-downBottom-up
Writing itclosest to the recursion; fastest to get rightneeds the order worked out
States computedonly those actually reachableall of them
Recursion depthcan overflow the stack (Python: about 1,000)none
Speedcall and cache overheadtight loops
Saving memoryhardeasy: keep only the rows still needed
Timestates × work per state
Spacestates, or less with rolling rows

In the grid each row reads only the row above, so one row of length cols is enough: O(cols) memory instead of O(rows · cols). Fibonacci-style recurrences need only the last two values.