Dynamic Programming Field Notes
Recursion that remembers. Fibonacci in 1-D, unique paths in 2-D, and the same four moves both times: brute force, find the repeats, add a cache, flip it bottom-up. Every diagram shows which call was saved.
0Why dynamic programming exists
A homework set where the same small sub-question shows up inside ten different problems. One student re-solves it ten times. Another solves it once, writes the answer in the margin of the notebook, and just looks it up the other nine times. Same answers, a tenth of the work. The notebook is the cache. Dynamic programming is "recursion with a notebook".
NeetCode says it plainly: DP is considered one of the hardest topics in CS, mostly because people overcomplicate it. Some DP problems are very hard, but the concept is not. Dynamic programming (DP) is an optimized version of recursion. You take a big problem, break it into smaller problems, and make sure each small problem is solved only once. That is usually better in time, and often in space, than plain recursion.
Start with the recursion you already know. Fibonacci, F(n) = F(n-1) + F(n-2):
def bruteForce(n):
if n <= 1:
return n
return bruteForce(n - 1) + bruteForce(n - 2)
Every call makes two more calls, so each level of the call tree doubles. Time is O(2n). Look at the tree for bruteForce(5) and count how often the same subtree appears.
Two words you need for the rest of the page. A subproblem is a smaller copy of the same question: F(3) is a subproblem of F(5). Subproblems are overlapping when the same one is asked more than once. Overlapping subproblems are the signal that DP will help.
11-D DP, top-down: recursion plus a cache
Memoization is a fancy word for caching
Memoization means: once a computation is done, store the result in a cache (a hash map or an array). If the same computation is asked again, retrieve it from the cache instead of recomputing. For fibonacci the hash map key is the index n and the value is F(n). A hash map lookup is O(1).
Recursion with memoization is called top-down DP, because you still start at the root of the tree (F(5)) and recurse down to the base cases. Only three lines change compared with the brute force:
def memoization(n, cache):
if n <= 1:
return n
if n in cache: # ② new base case: already solved? return it
return cache[n]
cache[n] = memoization(n - 1, cache) + memoization(n - 2, cache)
return cache[n] # ③ store, then return
memoization(5, {}) # ① the cache is passed in as a parameter → 5
- Add a cache parameter (a hash map).
- Add a base case: if
nis in the cache, return the cached value instead of recursing. - After computing F(n), store it in the cache before returning.
Time O(n): each fibonacci number is computed once. Space O(n): the cache holds up to n entries, and the recursion stack is also n deep at its deepest point.
In real Python: functools.cache
Python ships the cache as a decorator. @lru_cache(maxsize=None) or the shorter @cache (Python 3.9+) wraps a function so that every call with the same arguments returns the stored result. You write the brute force and add one line above it.
from functools import lru_cache, cache
@lru_cache(maxsize=None) # unlimited cache, keyed by the arguments
def fib_lru(n):
if n <= 1:
return n
return fib_lru(n - 1) + fib_lru(n - 2)
@cache # same thing, shorter (3.9+)
def fib_cache(n):
if n <= 1:
return n
return fib_cache(n - 1) + fib_cache(n - 2)
fib_cache(100) # 354224848179261915075, instantly
fib_lru.cache_info() # shows hits and misses - proof the cache is working
The decorator does exactly the three edits from the lesson, for you: it adds the hidden hash map, checks it before running the body, and stores the result after. In an interview say "I will memoize this with a dictionary" and then write either version. Arguments must be hashable (int, str, tuple - not list) because they become dictionary keys.
21-D DP, bottom-up: fill a table, then shrink it
Tabulation: base cases first, work up
Top-down starts at F(5) and asks for smaller values. Bottom-up DP (also called tabulation) goes the other way: compute the base cases first, then use them to build the next value, and so on up to n. No recursion. Use an array of size n + 1 where index i holds F(i). For n = 5 the finished array is [0, 1, 1, 2, 3, 5].
def tabulation(n):
if n < 2:
return n
table = [0] * (n + 1) # index i will hold F(i)
table[1] = 1 # base cases: table[0] = 0, table[1] = 1
for i in range(2, n + 1):
table[i] = table[i - 1] + table[i - 2]
return table[n]
Do we need the whole array? Only the last two
Look at the arrows in the picture: cell 5 reads only cells 3 and 4. Cells 0, 1, 2 are never touched again. We only ever use the previous two values, so keep just those two and compute in order. NeetCode uses an array of size 2, dp = [F(i-2), F(i-1)], and slides it forward with a temporary variable: store dp[1] in tmp, compute the next number into dp[1], then overwrite dp[0] with tmp. Space drops to O(1).
def dp(n):
if n < 2:
return n
dp = [0, 1] # [F(0), F(1)] - only two slots, ever
i = 2
while i <= n:
tmp = dp[1] # ① remember F(i-1) before it is overwritten
dp[1] = dp[0] + dp[1] # ② F(i) = F(i-2) + F(i-1)
dp[0] = tmp # ③ slide: old F(i-1) becomes the new "two back"
i += 1
return dp[1]
tmp exists only because ② destroys the old dp[1] before ③ needs it. Time O(n) (we iterate up to n), space O(1) (always an array of size 2).| i | tmp = dp[1] | dp[0] | dp[1] | meaning |
|---|---|---|---|---|
| start | - | 0 | 1 | dp = [F(0), F(1)] |
| 2 | 1 | 1 | 1 | dp[1] = 0 + 1; dp = [F(1), F(2)] |
| 3 | 1 | 1 | 2 | dp[1] = 1 + 1; dp = [F(2), F(3)] |
| 4 | 2 | 2 | 3 | dp[1] = 1 + 2; dp = [F(3), F(4)] |
| 5 | 3 | 3 | 5 | dp[1] = 2 + 3; dp = [F(4), F(5)] |
| 6 | i <= n is False. Return dp[1] = 5. | |||
The idiomatic Python version
Python's tuple assignment evaluates the whole right side before assigning, so tmp disappears. Same algorithm, same O(n) time and O(1) space.
def fib_two_vars(n):
a, b = 0, 1 # a = F(i), b = F(i+1)
for _ in range(n):
a, b = b, a + b # slide both by one; right side computed first
return a
Every subproblem is identified by one number, n, and its result fits in a one-dimensional structure of size proportional to n (the cache, or the table). That is 1-D DP. When a subproblem needs two numbers to describe it, you get 2-D DP, which is the next lesson.
32-D DP: unique paths in a grid
City blocks. You stand at the top-left corner and want the bottom-right corner. Streets only go down or right. How many different walks are there? Ask the same question from any corner: "how many ways from here?" The answer from a corner is the ways from the corner below plus the ways from the corner to the right. That is a subproblem with two coordinates, r and c.
When the result of a subproblem depends on two variables, the cache is a grid instead of a list: that is 2-D DP. The problem from the lesson: count the number of unique paths from the top-left to the bottom-right of a grid with rows rows and cols columns, moving only down or right. Because you only ever increase r or c, the only way to leave the grid is r == rows or c == cols.
Brute force: two choices per step
From (0, 0) you have at most two choices, down or right. The number of unique paths from a coordinate is the sum of the two recursive calls. Base cases: out of bounds returns 0 (that path is dead), the bottom-right cell returns 1 (one complete path).
# Brute Force - Time: O(2 ^ (n + m)), Space: O(n + m)
def bruteForce(r, c, rows, cols):
if r == rows or c == cols: # fell off the grid → 0 paths
return 0
if r == rows - 1 and c == cols - 1: # reached the target → 1 path
return 1
return (bruteForce(r + 1, c, rows, cols) + # go down
bruteForce(r, c + 1, rows, cols)) # go right
bruteForce(0, 0, 4, 4) # 20
Top-down: cache the answer for each coordinate
Same code plus a cache parameter. The cache is a rows × cols grid of zeros, and cache[r][c] stores the number of unique paths from that coordinate. NeetCode checks cache[r][c] > 0 to mean "already computed"; that works here because every in-bounds cell has at least one path, so 0 can only mean "not yet computed".
# Memoization - Time and Space: O(n * m)
def memoization(r, c, rows, cols, cache):
if r == rows or c == cols:
return 0
if cache[r][c] > 0: # already solved this coordinate
return cache[r][c]
if r == rows - 1 and c == cols - 1:
return 1
cache[r][c] = (memoization(r + 1, c, rows, cols, cache) +
memoization(r, c + 1, rows, cols, cache))
return cache[r][c]
cache = [[0] * 4 for _ in range(4)] # rows x cols grid of zeros
memoization(0, 0, 4, 4, cache) # 20
Bottom-up: start from the target, build rows upward
The picture above tells you the iterative order. Values become known from the bottom-right, so the loop must run bottom to top and, inside each row, right to left. Order matters because every value comes from the base-case side: below and to the right. And just like fibonacci, you do not need the whole grid in memory. To compute a row you only need the row below it. Keep two rows: prevRow (the row below) and curRow (the one being built). When a row is finished, it becomes the new prevRow.
# Dynamic Programming - Time: O(n * m), Space: O(m), where m is num of cols
def dp(rows, cols):
prevRow = [0] * cols # imaginary row below the grid: all 0
for r in range(rows - 1, -1, -1): # bottom row first, up to row 0
curRow = [0] * cols
curRow[cols - 1] = 1 # right-most column is the base case: 1
for c in range(cols - 2, -1, -1): # right to left
curRow[c] = curRow[c + 1] + prevRow[c] # right neighbor + cell below
prevRow = curRow # slide up
return prevRow[0] # top-left of the last row built
dp(4, 4) # 20
prevRow and curRow exist at any time.Trace of dp(3, 3), every row. The answer for a 3 × 3 grid is 6; for 4 × 4 it is 20; for 3 × 7 it is 28.
| r | curRow start | c = 1 | c = 0 | curRow end | then prevRow = |
|---|---|---|---|---|---|
| init | prevRow = [0, 0, 0] | [0, 0, 0] | |||
| 2 | [0, 0, 1] | 1 + 0 = 1 | 1 + 0 = 1 | [1, 1, 1] | [1, 1, 1] |
| 1 | [0, 0, 1] | 1 + 1 = 2 | 2 + 1 = 3 | [3, 2, 1] | [3, 2, 1] |
| 0 | [0, 0, 1] | 1 + 2 = 3 | 3 + 3 = 6 | [6, 3, 1] | [6, 3, 1] |
| end | Loop over r is done. Return prevRow[0] = 6. | ||||
The idiomatic top-down version
With @cache the coordinate pair (r, c) becomes the dictionary key automatically, and the "is it cached?" check disappears from your code. A nested function keeps rows and cols in scope so the cache key stays small.
from functools import cache
def uniquePaths_cache(rows, cols):
@cache
def go(r, c):
if r == rows or c == cols:
return 0
if r == rows - 1 and c == cols - 1:
return 1
return go(r + 1, c) + go(r, c + 1)
return go(0, 0)
uniquePaths_cache(3, 7) # 28
Top-down vs bottom-up, side by side
| Top-down (memoization) | Bottom-up (tabulation) | |
|---|---|---|
| Direction | Root of the tree down to the base cases | Base cases up to the answer |
| Style | Recursion + a cache (hash map or grid) | Loops + a table (array or grid) |
| Order of subproblems | Found automatically by the recursion | You must choose it: from the base-case side (left to right, or bottom-right to top-left) |
| Work done | Only the subproblems actually needed | Every cell of the table, needed or not |
| Time | O(n) / O(n·m) | O(n) / O(n·m) |
| Space | cache + recursion stack | table, often shrinkable to O(1) or O(m) |
| Risk | Recursion limit on deep inputs (Python default about 1000 frames) | Wrong fill order reads a cell that is still 0 |
| Python tool | @cache on the brute force | dp = [...], two variables, or prevRow / curRow |
| Reach for it when | You have the recursion and want a fast, safe speed-up | You need the best space, or n is too deep for recursion |
4The 4-step DP recipe
Both lessons follow the same four moves. This is the order to use in an interview, out loud, and you can stop at any step that is good enough for the constraints.
- Write the brute-force recursion. Decide what one call means ("paths from (r, c)", "F(n)"), write the base cases, and write the choices as recursive calls added or compared together. This is the recurrence: the formula that relates a problem to its smaller copies.
- Find the repeated subproblems. Draw the call tree for a small input. If the same call appears twice (F(2) three times, (1, 1) twice), you have overlapping subproblems and DP will pay off. The parameters that identify a call are the state: one number → 1-D, two numbers → 2-D.
- Add a cache. Check the cache before recursing, store after computing. In Python:
@cache, or an explicit dictionary / grid parameter. Time drops from exponential to (number of states) × (work per state). - Flip to bottom-up and shrink the space. Read the fill order off the cache picture (from the base cases toward the answer), replace recursion with loops, then keep only the rows or values the recurrence actually reads: two variables for fibonacci, two rows for the grid.
Most people who "find DP hard" try to write the bottom-up table first. The table is step 4. The recursion is where you find the states and the base cases; the table is only a re-ordering of it. Write the slow version, make it correct on a tiny input, then speed it up.
5Moves that solve most DP problems
The lessons teach fibonacci and unique paths. Interviews reuse the same two shapes with different words. Learn the shapes, and learn how to notice that a problem is one of them.
Move 1: The memo decorator (top-down template)
When to use: you have a correct recursion and it is too slow. Wrap it. Works for any number of state parameters, as long as they are hashable.
from functools import cache
def solve_with_memo(n):
@cache
def f(i): # f(i) = answer for state i
if i <= 1: # base case(s)
return i
return f(i - 1) + f(i - 2) # recurrence: combine smaller states
return f(n)
Move 2: The 1-D "two back" family: dp[i] from dp[i-1] and dp[i-2]
When to use: a line of positions, and the answer at position i depends only on the one or two positions before it. Fibonacci, climbing stairs, house robber, min cost climbing stairs all have this shape. Keep two variables and slide.
# Climbing Stairs (LC 70): ways to reach the top taking 1 or 2 steps at a time
def climbStairs(n):
one, two = 1, 1 # ways from step n, and from step n-1 (both 1)
for _ in range(n - 1):
one, two = one + two, one # exactly the fibonacci slide
return one # climbStairs(5) → 8
# House Robber (LC 198): rob non-adjacent houses for the max total
def rob(nums):
rob1, rob2 = 0, 0 # best total ending two back, and one back
for n in nums:
rob1, rob2 = rob2, max(rob1 + n, rob2) # rob this house, or skip it
return rob2 # rob([2, 7, 9, 3, 1]) → 12
# Min Cost Climbing Stairs (LC 746): cheapest way past the last step
def minCostClimbingStairs(cost):
a, b = 0, 0 # min cost to stand on step i-2, i-1
for i in range(2, len(cost) + 1):
a, b = b, min(b + cost[i - 1], a + cost[i - 2])
return b # minCostClimbingStairs([10, 15, 20]) → 15
Move 3: The 2-D grid family: cell from below and right (or above and left)
When to use: a grid, moves restricted to two directions, and a question about counting or optimizing paths. Unique paths counts (plus), min path sum optimizes (min). Same two-row loop; only the combine step changes, and for a cost grid you add the cell's own value.
# Minimum Path Sum (LC 64): cheapest down/right path from top-left to bottom-right
def minPathSum(grid):
rows, cols = len(grid), len(grid[0])
prev = [float("inf")] * cols # "below the grid" is impossible → infinity
for r in range(rows - 1, -1, -1):
cur = [0] * cols
for c in range(cols - 1, -1, -1):
if r == rows - 1 and c == cols - 1:
cur[c] = grid[r][c] # base case: the target itself
else:
right = cur[c + 1] if c + 1 < cols else float("inf")
down = prev[c]
cur[c] = grid[r][c] + min(right, down) # min instead of +
prev = cur
return prev[0] # minPathSum([[1,3,1],[1,5,1],[4,2,1]]) → 7
Move 4: How to spot DP in a problem statement
Three kinds of questions, each answered over a series of choices, are DP suspects:
- "How many ways" to reach / build / arrange something (unique paths, climbing stairs, coin change II). The recurrence adds.
- "Minimum or maximum" cost, length, profit, sum over a sequence of choices (min path sum, house robber, longest increasing subsequence, coin change). The recurrence takes min or max.
- "Is it possible / true or false" to reach a state (word break, partition equal subset sum). The recurrence uses
or.
Then confirm the two conditions. Optimal substructure: the best answer for the big problem is built from best answers of smaller ones. Overlapping subproblems: the brute force asks the same smaller question more than once. If the second condition fails (every subproblem is different, like merge sort), plain recursion is already fine and a cache buys nothing.
6Big-O summary
n is the fibonacci index; for grids, n is rows and m is columns. The pattern is always the same: exponential brute force, then (number of states) for time, then shrink the space.
| Version | Time | Space | Why |
|---|---|---|---|
| Fibonacci, brute force | O(2^n) | O(n) | each call makes 2 calls, tree depth n; space is the recursion stack |
| Fibonacci, memoization (top-down) | O(n) | O(n) | each F(i) computed once; cache of n entries plus a stack n deep |
| Fibonacci, bottom-up table | O(n) | O(n) | one loop; array of size n + 1 |
| Fibonacci, two variables | O(n) | O(1) | only the last two values are ever read |
| Unique paths, brute force | O(2^(n+m)) | O(n + m) | 2 choices per step, about n + m steps; stack depth is the path length |
| Unique paths, memoization | O(n · m) | O(n · m) | each cell computed once; cache is the whole grid |
| Unique paths, bottom-up two rows | O(n · m) | O(m) | still every cell once, but only prevRow and curRow are kept |
| Hash-map cache lookup | O(1) | - | that is why the check "is it cached?" costs nothing |
General rule to say in interviews: memoized time = (number of distinct states) × (work per state, not counting the recursive calls). Fibonacci: n states × O(1) = O(n). Grid: n · m states × O(1) = O(n · m).
7Mistakes to watch for
You add the if n in cache check but forget cache[n] = ... before returning. The cache stays empty and the function is still O(2^n). Store, then return the stored value.
The lookup goes before the recursive calls and after the cheap base cases. If you put it after the recursion, you recurse anyway. If you put it before the out-of-bounds check in the grid code, you index cache[r][c] with r = rows and crash.
NeetCode's cache[r][c] > 0 is safe for unique paths because every cell has at least one path. For problems where 0 (or a negative) is a valid result, use -1 as the empty marker, or a dictionary with in, or @cache.
curRow[c] = curRow[c + 1] + prevRow[c] reads the right neighbor, so the inner loop must go right to left. Going left to right reads a curRow[c + 1] that is still 0 and every answer becomes 0 or 1. Rule: iterate from the base-case side toward the answer.
In the two-variable version, dp[0] = dp[1] followed by dp[1] = dp[0] + dp[1] doubles instead of adding. Save tmp = dp[1] first, or use Python's tuple swap a, b = b, a + b, which reads the whole right side first.
The table must have n + 1 slots so table[n] exists. The loop must run while i <= n (or range(2, n + 1)). range(2, n) stops one early and returns F(n - 1).
def memo(n, cache={}) creates one dictionary shared by every call of the function for the life of the program. For fibonacci that happens to be harmless; for a function whose answer depends on other inputs (a different grid) it returns stale results. Pass the cache in, or create it inside a wrapper, or use @cache on a nested function.
@cache keys on the arguments. Passing a list (go(nums, i)) raises TypeError: unhashable type. Pass only the indices; keep the array in the enclosing scope.
Top-down on n = 5000 raises RecursionError in Python (default limit near 1000 frames). Either sys.setrecursionlimit or, better, use the bottom-up loop. This is a real reason interviewers ask for step 4 of the recipe.
Interview one-liners
- "DP is recursion where each subproblem is solved once and cached; it turns O(2^n) fibonacci into O(n)."
- "Memoization is top-down: recurse from the root and store results in a hash map keyed by the state."
- "Tabulation is bottom-up: fill a table from the base cases, in the order the recurrence needs."
- "The state is whatever identifies a subproblem: one number means 1-D DP, two numbers means 2-D DP."
- "Memoized time is number of states times work per state; here n states times O(1)."
- "Fibonacci only reads the previous two values, so I keep two variables and get O(1) space; the grid only reads the row below, so I keep two rows and get O(m) space."
- "In Python I write the brute force, add
@cache, then flip to loops if the input is deep enough to hit the recursion limit."
8Practice, in this order
Do each one in the four steps: brute force, find the repeat, add @cache, flip to bottom-up. Even when the easy version is enough, write the bottom-up version once. That is the muscle.
| # | Problem | Move it trains | Level |
|---|---|---|---|
| 509 | Fibonacci Number | the lesson exactly: memo, then two variables | Easy |
| 70 | Climbing Stairs | 1-D "two back", fibonacci in disguise | Easy |
| 1137 | N-th Tribonacci Number | 1-D "three back", slide three variables | Easy |
| 746 | Min Cost Climbing Stairs | 1-D "two back" with min | Easy |
| 198 | House Robber | 1-D "two back" with max, rob or skip | Medium |
| 213 | House Robber II | run House Robber twice on a circle | Medium |
| 62 | Unique Paths | 2-D grid, the lesson exactly, two rows | Medium |
| 63 | Unique Paths II | 2-D grid with obstacles (cell = 0) | Medium |
| 64 | Minimum Path Sum | 2-D grid with min instead of plus | Medium |
| 1143 | Longest Common Subsequence | 2-D DP on two strings, state (i, j) | Medium |
| 322 | Coin Change | 1-D min over choices, unbounded | Medium |
| 300 | Longest Increasing Subsequence | 1-D max, dp[i] looks at all j < i | Medium |
Your next step today: LeetCode 70, Climbing Stairs. Write the brute force, draw the tree for n = 5, add @cache, then write the two-variable loop from memory and compare with the fibonacci trace table.
9Self-check
Answer out loud before opening. If you cannot answer in one breath, reread that section.
Q1In one sentence, what is dynamic programming?
An optimized version of recursion: break the problem into smaller subproblems and solve each subproblem only once by caching its result.
Q2Why is bruteForce(n) for fibonacci O(2^n)? Which value is repeated in the tree for n = 5, and how many times?
Every call makes two more calls, so each level of the tree doubles and the depth is n. In the tree for F(5), F(2) is computed three times and F(3) twice.
Q3What three changes turn the brute force into memoization(n, cache)?
1. Add a cache parameter (hash map). 2. Add a base case: if n is in the cache, return the cached value. 3. After computing F(n), store it in the cache before returning.
Q4Why is memoized fibonacci O(n) time and O(n) space?
Each fibonacci number is computed once and a cache lookup is O(1), so n computations total. The cache holds up to n entries, and the recursion stack is also up to n deep.
Q5What do "top-down" and "bottom-up" mean?
Top-down: start at the root (F(n)) and recurse down to the base cases, caching on the way back. Bottom-up: compute the base cases first and build up to n with a loop, no recursion.
Q6Trace it: run dp(6) from the lesson. Give (i, dp[0], dp[1]) after every loop turn and the return value.
Start [0, 1]. i=2: [1, 1]. i=3: [1, 2]. i=4: [2, 3]. i=5: [3, 5]. i=6: [5, 8]. Loop stops because 7 > 6. Return dp[1] = 8.
Q7Why does the two-variable version need tmp? What Python line removes it?
dp[1] = dp[0] + dp[1] destroys the old dp[1], but dp[0] must become that old value next. tmp saves it. a, b = b, a + b evaluates the whole right side before assigning, so no temporary is needed.
Q8What makes unique paths a 2-D problem instead of 1-D?
A subproblem is identified by two variables, the row r and the column c. So the cache is a rows × cols grid, one slot per (r, c).
Q9In the grid brute force, which two cells do the same work twice, and why?
Any cell reachable by more than one route, for example (1, 1): down-then-right and right-then-down both arrive there, so bruteForce(1, 1) and its whole subtree run twice. The brute force has no memory of earlier calls.
Q10Trace it: run the bottom-up dp(2, 3). Give prevRow after each row and the answer.
init [0, 0, 0]. r=1: curRow starts [0, 0, 1], c=1: 1 + 0 = 1, c=0: 1 + 0 = 1 → [1, 1, 1]. r=0: [0, 0, 1], c=1: 1 + 1 = 2, c=0: 2 + 1 = 3 → [3, 2, 1]. Return prevRow[0] = 3.
Q11Why does the bottom-up grid loop go right to left inside each row, and bottom to top across rows?
curRow[c] = curRow[c + 1] + prevRow[c]: the value comes from the right neighbor and the cell below, which is the base-case side. Those must already be computed, so you fill from the bottom-right toward the top-left.
Q12How does the space drop from O(n·m) to O(m) in the grid, and what is the fibonacci equivalent?
Computing a row only needs the row below it, so keep prevRow and curRow and drop the rest: two rows of m cells. Fibonacci only needs the previous two values, so keep two variables: O(1).
Q13What does @cache from functools do, and what must be true of the arguments?
It wraps the function with a hidden dictionary: same arguments → return the stored result instead of running the body. Arguments must be hashable (int, str, tuple), because they become the dictionary key. Lists are not allowed.