Part 14 · Lesson 54 · read

Dynamic Programming From Recursion

Repeated subproblems, memoisation, tabulation and saving memory.

On this page

  1. The same work, again and again
  2. Two properties that make DP work
  3. The four questions
  4. Step 1: plain recursion
  5. Step 2: memoization (top-down)
  6. Traced: climbing stairs with a cache
  7. Step 3: tabulation (bottom-up)
  8. Step 4: reducing space
  9. Climbing stairs and minimum cost
  10. When greedy fails, DP still works
  11. Practice questions (7)
  12. Make it stick
  13. What's next

The same work, again and again

Part 14 is about one big idea: dynamic programming (DP). DP is a method that solves a problem by splitting it into smaller versions of itself, solving each small version only once, and saving its answer. The name sounds grand, but it is just “recursion that remembers”. Recursion means a function that calls itself. You break a problem into smaller copies of itself, as in Part 8. But you never solve the same small copy twice.

Lesson 32 introduced recursion trees, where every call is drawn as a box (a node). Here is the plain Fibonacci function. In Fibonacci, each number is the sum of the two numbers before it. This version also counts how many calls it makes:

JavaScript
let calls = 0;function fib(n) {                            // fib(0) = 0, fib(1) = 1, fib(n) = fib(n-1) + fib(n-2)  calls++;  if (n <= 1) return n;                      // base cases  return fib(n - 1) + fib(n - 2);}console.log(fib(20), calls); // 6765 21891

To compute fib(20), the function runs 21,891 times. For 30 it is much worse:

JavaScript
let calls = 0;function fib(n) {  calls++;  return n <= 1 ? n : fib(n - 1) + fib(n - 2);}console.log(fib(30), calls); // 832040 2692537

That is nearly 2.7 million calls for the 30th term. Each call to fib(n) starts two more calls, so the tree about doubles at each level. The reason is easy to see in the tree. fib(5) calls fib(4) and fib(3), but fib(4) also calls fib(3). So the fib(3) part of the tree is built twice, and inside it fib(2) is built again and again. There are only n + 1 different questions, but we answer them an enormous number of times (exponentially many).

Two properties that make DP work

DP works when a problem has both of these properties:

Dry run · the DP checklist
PropertyMeaningExampleWhat it gives us
Overlapping subproblemsthe same smaller question comes up many timesfib(3) is needed by both fib(5) and fib(4)saving answers (caching) pays off
Optimal substructurethe best answer is built from the best answers to smaller questionsthe cheapest way to reach step 5 uses the cheapest way to reach step 4 or step 3the transition formula is correct

Fibonacci and climbing stairs do not look for a smallest or biggest value, so optimal substructure is true there without any effort. It starts to matter when a problem asks for a minimum, a maximum or a count.

A subproblem is just “the same question with a smaller input”. Sometimes subproblems do not overlap. Merge sort's two halves never share work, so saving answers does nothing, and plain divide and conquer (split, solve each part, join) is the right tool. If subproblems do overlap, DP turns a very slow (exponential, like 2ⁿ) solution into a fast (polynomial, like n or n²) one.

The four questions

Every DP solution answers the same four questions. Write them down before you write any code:

  1. State. Which few values pick out one subproblem? (For Fibonacci: just i.)
  2. Transition. How do you build the answer for a state from the answers to smaller states?
  3. Base cases. Which states have an answer right away, with no recursion?
  4. Order. In what order should you fill the states, so that everything a state needs is already known? (For most 1-D problems: from small to large.)
Dry run · the four questions, applied
ProblemStateTransitionBase caseOrder
Fibonaccii: which numberdp[i] = dp[i-1] + dp[i-2]dp[0] = 0, dp[1] = 1i upwards
Climbing stairsi: which stepdp[i] = dp[i-1] + dp[i-2]dp[0] = 1, dp[1] = 1i upwards
Min cost climbingi: which step we stand ondp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])dp[0] = dp[1] = 0i upwards
Fewest coinsa: the amount leftdp[a] = 1 + min(dp[a - c]) over coins c ≤ adp[0] = 0a upwards

When you can write the transition as a formula, the three ways of coding it below are mostly a copy of the formula into code.

Step 1: plain recursion

Always start here. Write the recursion that is clearly correct, even if it is slow. It is your plan for the solution, and it shows you the state and the transition. The Fibonacci function at the top of this lesson is already this first step. (The brute-force coin counter in lesson 47 is another one: “try every first coin”.)

Step 2: memoization (top-down)

Memoization is a technique that saves (remembers) the result of each call, so the same call is never worked out twice. The word comes from “memo”, a note to yourself. It is not “memorization”. You keep the recursion exactly as it was and add a notebook. Before you work out a state, look it up in the notebook. After you work it out, write it down. The notebook is called a cache: a place where you store answers you already worked out, so you can reuse them. It is usually a Map (lesson 32 used the same trick) or an array indexed by the state. It is called top-down because you start from the big question and let the recursion find the smaller ones.

JavaScript
let calls = 0;function fibMemo(n, memo = new Map()) {  calls++;  if (n <= 1) return n;                      // base cases  if (memo.has(n)) return memo.get(n);       // already solved: reuse the answer  const answer = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);  memo.set(n, answer);                       // write the answer down before returning  return answer;}console.log(fibMemo(30), calls); // 832040 59calls = 0;console.log(fibMemo(50), calls); // 12586269025 99

Now fibMemo(30) makes 59 calls instead of 2.7 million. Why 59? Each of the 29 states from 2 to 30 does real work only once (that is 29 calls that go on to call more). Each of those makes two calls, 58 in total. Add the first call: 1 + 58 = 59. In general it is about 2n calls, so the time is O(n). The notebook uses O(n) space, and the recursion depth is O(n) too.

Traced: climbing stairs with a cache

Here is the stairs problem you will meet soon. ways(n) counts the ways to climb n stairs if you take 1 or 2 steps at a time. Watch the notebook fill up. Look for the cache hits. These are the moments when a whole branch of the tree is skipped.

step 1 / 28
Set up
memo = empty Map

The notebook where we will write down every answer we work out.

▶const memo = new Map();2function ways(n) {3  if (n <= 1) return 1;4  if (memo.has(n)) return memo.get(n);5  const result = ways(n - 1) + ways(n - 2);6  memo.set(n, result);7  return result;8}9ways(5);
Variables
memoMap(0) {}
Console
(nothing printed yet)
Fifteen calls become nine. The hit on ways(2) saves a part of the tree under ways(4), and the hit on ways(3) saves another part under ways(5). · interactive

Step 3: tabulation (bottom-up)

Tabulation goes the other way. Skip the recursion. Make a table dp (a plain array) and fill in the base cases. Then loop from small states to large ones, so every value you need is already in the table. This is called bottom-up.

JavaScript
function fibTab(n) {  if (n <= 1) return n;  const dp = new Array(n + 1).fill(0);       // dp[i] = fib(i)  dp[0] = 0;                                 // base cases first  dp[1] = 1;  for (let i = 2; i <= n; i++) {             // fill in an order where the inputs already exist    dp[i] = dp[i - 1] + dp[i - 2];  }  return dp[n];}console.log(fibTab(10)); // 55console.log(fibTab(50)); // 12586269025

There is no call stack to overflow (the call stack is the computer's list of unfinished function calls), and the loop is short and fast. The cost is that you must get the order right yourself. Also, you fill every entry, even if the final answer needs only some of them.

Step 4: reducing space

Look at the transition: dp[i] reads only dp[i-1] and dp[i-2]. Older entries are never used again, so the full table wastes space. Keep only the last two values:

JavaScript
function fibSpace(n) {  if (n <= 1) return n;  let prev = 0, curr = 1;                    // fib(i-2) and fib(i-1): all that the next value needs  for (let i = 2; i <= n; i++) {    [prev, curr] = [curr, prev + curr];      // slide the window one step to the right  }  return curr;}console.log(fibSpace(10)); // 55console.log(fibSpace(50)); // 12586269025
Dry run · the same problem, four ways
StyleTimeSpaceNotes
Plain recursionO(φⁿ), about 1.6ⁿ (φ is about 1.618)O(n) call stacknever reuses any answer
Memoization (top-down)O(n)O(n) cache + O(n) call stackeasy to write from the recursion; only works out what is needed
Tabulation (bottom-up)O(n)O(n) tableno recursion, so no stack overflow; fills every entry
Space-reducedO(n)O(1)keep only the last few entries

A simple rule: write the recursion first. Add the memo if it is slow. Change to a table if you want to avoid recursion. Make the table smaller last, and only if the transition looks back a fixed distance.

Recursion depth in JavaScript
The memoized version goes n levels deep. When n is in the tens of thousands, the call stack can overflow (run out of room). This is one practical reason to prefer the bottom-up loop on large inputs.

Climbing stairs and minimum cost

Climbing stairs: you can take 1 or 2 steps at a time. In how many different ways can you reach step n? Your last move was either a 1-step from step n − 1 or a 2-step from step n − 2. These two cases never overlap, so ways(n) = ways(n-1) + ways(n-2). It is Fibonacci in disguise, but with different base cases. There is one way to reach step 0 (do nothing) and one way to reach step 1.

JavaScript
// Climbing stairs: each move is 1 or 2 steps. In how many ways can you reach step n?// ways(n) = ways(n-1) + ways(n-2): your last move was a 1-step from n-1, or a 2-step from n-2.function waysMemo(n, memo = new Map()) {     // top-down  if (n <= 1) return 1;                      // one way to stand still, one way to reach step 1  if (memo.has(n)) return memo.get(n);  const answer = waysMemo(n - 1, memo) + waysMemo(n - 2, memo);  memo.set(n, answer);  return answer;}function waysTab(n) {                        // bottom-up, O(1) space  let prev = 1, curr = 1;                    // ways(0) and ways(1)  for (let i = 2; i <= n; i++) [prev, curr] = [curr, prev + curr];  return curr;}console.log(waysMemo(5), waysTab(5));   // 8 8console.log(waysMemo(10), waysTab(10)); // 89 89

Min cost climbing stairs gives each stair a price and asks for the cheapest route. The state is the step we are standing on. The transition picks the cheaper of the two ways to arrive. Here optimal substructure does real work: the cheapest route to step i must contain a cheapest route to the step it came from.

JavaScript
// Min cost climbing stairs: cost[i] is paid when you step OFF stair i (1 or 2 steps up).// Start on stair 0 or 1 for free; the goal is "one past the last stair", index n.function minCostClimbingStairs(cost) {  const n = cost.length;  const dp = new Array(n + 1).fill(0);       // dp[i] = cheapest way to be standing on step i  for (let i = 2; i <= n; i++) {    dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);  }  return dp[n];}console.log(minCostClimbingStairs([10, 15, 20]));                        // 15console.log(minCostClimbingStairs([1, 100, 1, 1, 1, 100, 1, 1, 100, 1])); // 6

When greedy fails, DP still works

Lesson 47 ended with an example where greedy fails. The coins are 1, 3 and 4, and the amount is 6. The greedy rule “take the biggest coin that fits” gives 4 + 1 + 1 (three coins). But 3 + 3 needs only two coins. Greedy picks one choice and never goes back. DP instead tries every possible last coin and trusts the best answer it already worked out for the rest.

JavaScript
// Fewest coins for an amount. Lesson 47: greedy (biggest coin first) can be wrong.function greedyCoins(coins, amount) {  const sorted = [...coins].sort((a, b) => b - a);  let count = 0;  for (const c of sorted) {    count += Math.floor(amount / c);    amount %= c;  }  return amount === 0 ? count : -1;}// DP: dp[a] = fewest coins that add up to exactly a.function dpCoins(coins, amount) {  const dp = new Array(amount + 1).fill(Infinity);  dp[0] = 0;                                 // zero coins make amount 0  for (let a = 1; a <= amount; a++) {    for (const c of coins) {      if (c <= a) dp[a] = Math.min(dp[a], dp[a - c] + 1);   // try c as the last coin    }  }  return dp[amount] === Infinity ? -1 : dp[amount];}console.log(greedyCoins([1, 3, 4], 6)); // 3   (4 + 1 + 1)console.log(dpCoins([1, 3, 4], 6));     // 2   (3 + 3)
Dry run · dp[a] for coins 1, 3, 4
Amount adp[a]How
00base case
111 + dp[0]
221 + dp[1]
311 + dp[0] using the coin 3
411 + dp[0] using the coin 4
521 + dp[1] using the coin 4
621 + dp[3] using the coin 3, which is 3 + 3

Lesson 55 covers this fully as the coin change problem. For now, notice that it has the same four parts as Fibonacci.

Practice questions

For each question, first write the state, the transition and the base cases. Then code the plain recursion, add the cache, and try to make it smaller.

Q1Fibonacci numberEasy

The Fibonacci numbers follow these rules: F(0) = 0, F(1) = 1 and F(n) = F(n − 1) + F(n − 2) for n > 1. Given n, return F(n).

Example 1
Input: n = 2
Output: 1
Explanation: F(2) = F(1) + F(0) = 1 + 0.
Example 2
Input: n = 4
Output: 3
Explanation: F(4) = F(3) + F(2) = 2 + 1.
Example 3
Input: n = 0
Output: 0
Explanation: F(0) = 0 by definition.
Stuck? Open one hint at a time
Hint 1
The definition is already a recursion (a function that calls itself): F(n) = F(n-1) + F(n-2).
Hint 2
Draw the tree of calls for F(5). Which parts of the tree repeat?
Hint 3
Each value depends only on the two before it. How many variables do you need?
Show all 3 solutions with explanations
Approach 1Plain recursion
The idea

Copy the definition into code. It is correct, but every call starts two more calls.

JavaScript
function fib(n) {  if (n <= 1) return n;  return fib(n - 1) + fib(n - 2);}console.log(fib(2)); // 1console.log(fib(4)); // 3console.log(fib(0)); // 0
How it works

The tree of calls has about 1.6ⁿ boxes, so the time grows exponentially (O(2ⁿ) is an easy upper limit). The space is the recursion depth, O(n). It is fine for n up to 30, but far too slow for bigger n.

Approach 2Memoization (top-down)
The idea

Use the same recursion plus a Map. Look the answer up before you work it out. Store it before you return.

JavaScript
function fib(n, memo = new Map()) {  if (n <= 1) return n;  if (memo.has(n)) return memo.get(n);  const answer = fib(n - 1, memo) + fib(n - 2, memo);  memo.set(n, answer);  return answer;}console.log(fib(2));  // 1console.log(fib(4));  // 3console.log(fib(30)); // 832040
How it works

Each of the n + 1 states is worked out once. The time is O(n). The space is O(n) for the cache and the call stack.

Approach 3Bottom-up with two variables
The idea

Go from 2 up to n and keep only the last two Fibonacci numbers.

JavaScript
function fib(n) {  if (n <= 1) return n;  let prev = 0, curr = 1;  for (let i = 2; i <= n; i++) {    [prev, curr] = [curr, prev + curr];  }  return curr;}console.log(fib(2));  // 1console.log(fib(4));  // 3console.log(fib(0));  // 0console.log(fib(30)); // 832040
How it works

The time is O(n) and the space is O(1). There is no recursion at all.

Which approach should you use?

In an interview, write the loop with two variables. If you mention the memo version first, it shows you understand why it works. (LeetCode 509.)

Q2Climbing stairsEasy

You are climbing a staircase with n steps. Each move climbs 1 or 2 steps. In how many different ways can you reach the top?

Example 1
Input: n = 2
Output: 2
Explanation: 1+1 or 2.
Example 2
Input: n = 3
Output: 3
Explanation: 1+1+1, 1+2 or 2+1.
Example 3
Input: n = 5
Output: 8
Explanation: ways(5) = ways(4) + ways(3) = 5 + 3.
Stuck? Open one hint at a time
Hint 1
Think about the very last move. Where were you standing before it?
Hint 2
You were on step n − 1 (then you took a 1-step) or on step n − 2 (then you took a 2-step). These two cases never overlap.
Hint 3
What are the answers for n = 0 and n = 1?
Show all 3 solutions with explanations
Approach 1Plain recursion
The idea

ways(n) = ways(n-1) + ways(n-2), with ways(0) = ways(1) = 1.

JavaScript
function climbStairs(n) {  if (n <= 1) return 1;  return climbStairs(n - 1) + climbStairs(n - 2);}console.log(climbStairs(2)); // 2console.log(climbStairs(3)); // 3console.log(climbStairs(5)); // 8
How it works

The time grows exponentially, because the answer for the same step is worked out again in many branches.

Approach 2Memoization
The idea

Save ways(n) in a Map (the cache).

JavaScript
function climbStairs(n, memo = new Map()) {  if (n <= 1) return 1;  if (memo.has(n)) return memo.get(n);  const answer = climbStairs(n - 1, memo) + climbStairs(n - 2, memo);  memo.set(n, answer);  return answer;}console.log(climbStairs(2));  // 2console.log(climbStairs(3));  // 3console.log(climbStairs(5));  // 8console.log(climbStairs(45)); // 1836311903
How it works

The time and space are O(n). The answer for each step is worked out once.

Approach 3Tabulation with O(1) space
The idea

Fill in answers going upwards. Remember only the last two answers.

JavaScript
function climbStairs(n) {  let prev = 1, curr = 1;                    // ways(0) and ways(1)  for (let i = 2; i <= n; i++) {    [prev, curr] = [curr, prev + curr];  }  return curr;}console.log(climbStairs(2));  // 2console.log(climbStairs(3));  // 3console.log(climbStairs(5));  // 8console.log(climbStairs(45)); // 1836311903
How it works

The time is O(n) and the space is O(1). The sequence is Fibonacci, moved by one place.

Which approach should you use?

Use the table (bottom-up) loop. Be ready to explain the recurrence (a formula that uses earlier answers): the last move was 1 step or 2 steps. That is the real point of the question. (LeetCode 70.)

Q3Min cost climbing stairsEasy

cost[i] is the price of stepping off stair i, after which you may climb 1 or 2 stairs. You may start on stair 0 or stair 1. Return the smallest cost to reach the top, which is just past the last stair.

Example 1
Input: cost = [10,15,20]
Output: 15
Explanation: Start on stair 1 (cost 15), then jump two stairs to the top.
Example 2
Input: cost = [1,100,1,1,1,100,1,1,100,1]
Output: 6
Explanation: Use the stairs at positions 0, 2, 4, 6, 7 and 9. They all cost 1. Skip every stair that costs 100.
Stuck? Open one hint at a time
Hint 1
The top is one position past the last stair, at position n. You pay cost[i] when you leave stair i.
Hint 2
Let dp[i] be the cheapest total cost to stand on position i. You came from position i − 1 or i − 2.
Hint 3
You can start on stair 0 or 1 for free, so dp[0] = dp[1] = 0.
Show all 2 solutions with explanations
Approach 1Recursion with memo (top-down)
The idea

best(i) is the cheapest cost to reach position i. The formula is best(i) = min(best(i-1) + cost[i-1], best(i-2) + cost[i-2]).

JavaScript
function minCostClimbingStairs(cost) {  const memo = new Map();  function best(i) {    if (i <= 1) return 0;                    // starting on stair 0 or 1 is free    if (memo.has(i)) return memo.get(i);    const answer = Math.min(best(i - 1) + cost[i - 1], best(i - 2) + cost[i - 2]);    memo.set(i, answer);    return answer;  }  return best(cost.length);}console.log(minCostClimbingStairs([10, 15, 20]));                         // 15console.log(minCostClimbingStairs([1, 100, 1, 1, 1, 100, 1, 1, 100, 1])); // 6
How it works

The time and space are O(n) (the cache plus the recursion). Without the memo the time would grow exponentially, just like Fibonacci.

Approach 2Bottom-up with two variables
The idea

Go upwards from position 2. Keep the cheapest cost of the last two positions.

JavaScript
function minCostClimbingStairs(cost) {  let twoBack = 0, oneBack = 0;              // dp[i-2] and dp[i-1], starting at i = 2  for (let i = 2; i <= cost.length; i++) {    const here = Math.min(oneBack + cost[i - 1], twoBack + cost[i - 2]);    twoBack = oneBack;    oneBack = here;  }  return oneBack;}console.log(minCostClimbingStairs([10, 15, 20]));                         // 15console.log(minCostClimbingStairs([1, 100, 1, 1, 1, 100, 1, 1, 100, 1])); // 6
How it works

The time is O(n) and the space is O(1). Step-by-step check on [10,15,20]: i = 2 gives min(0 + 15, 0 + 10) = 10; i = 3 gives min(10 + 20, 0 + 15) = 15.

Which approach should you use?

Use bottom-up with two variables. (LeetCode 746.)

Q4N-th Tribonacci numberEasy

The Tribonacci numbers are defined by T(0) = 0, T(1) = 1, T(2) = 1 and T(n+3) = T(n) + T(n+1) + T(n+2). Given n, return T(n).

Example 1
Input: n = 4
Output: 4
Explanation: T0 = 0, T1 = 1, T2 = 1, T3 = 2, T4 = 4 (each term is the sum of the three before it).
Example 2
Input: n = 25
Output: 1389537
Explanation: The same rule is applied 25 times.
Stuck? Open one hint at a time
Hint 1
This is Fibonacci with three terms: T(n) = T(n-1) + T(n-2) + T(n-3).
Hint 2
There are three base cases: T(0), T(1), T(2).
Hint 3
How many earlier values must you remember?
Show all 2 solutions with explanations
Approach 1Memoized recursion
The idea

Recurse on the three earlier terms and save each result in a cache.

JavaScript
function tribonacci(n, memo = new Map()) {  if (n === 0) return 0;  if (n <= 2) return 1;  if (memo.has(n)) return memo.get(n);  const answer = tribonacci(n - 1, memo) + tribonacci(n - 2, memo) + tribonacci(n - 3, memo);  memo.set(n, answer);  return answer;}console.log(tribonacci(4));  // 4console.log(tribonacci(25)); // 1389537
How it works

Each state makes three calls, but each state is solved only once. The time and space are O(n). Without the cache the time would grow exponentially.

Approach 2Bottom-up with three variables
The idea

Slide a window of three values along the sequence (always keep the latest three).

JavaScript
function tribonacci(n) {  if (n === 0) return 0;  if (n <= 2) return 1;  let a = 0, b = 1, c = 1;                   // T(i-3), T(i-2), T(i-1)  for (let i = 3; i <= n; i++) {    [a, b, c] = [b, c, a + b + c];  }  return c;}console.log(tribonacci(4));  // 4console.log(tribonacci(25)); // 1389537console.log(tribonacci(0));  // 0
How it works

The time is O(n) and the space is O(1). This is the Fibonacci loop with one more variable.

Which approach should you use?

Use the loop with three variables. The question tests whether you can change the Fibonacci pattern to a different number of earlier terms. (LeetCode 1137.)

Q5House robber (a preview of lesson 55)Medium

Houses in a row hold nums[i] money each. If you rob two houses next to each other, an alarm goes off. Return the most money you can rob without setting off the alarm.

Example 1
Input: nums = [1,2,3,1]
Output: 4
Explanation: Rob house 0 (1) and house 2 (3). 1 + 3 = 4.
Example 2
Input: nums = [2,7,9,3,1]
Output: 12
Explanation: Rob houses 0, 2 and 4: 2 + 9 + 1 = 12.
Stuck? Open one hint at a time
Hint 1
You cannot rob two houses next to each other. For each house you decide: skip it or rob it.
Hint 2
Let best(i) be the most money from houses 0..i. If you skip house i, you get best(i-1).
Hint 3
If you rob house i, you must have skipped house i − 1. You get nums[i] + best(i-2).
Show all 2 solutions with explanations
Approach 1Recursion with memo
The idea

State: house index. Transition: best(i) = max(best(i-1), nums[i] + best(i-2)). Base case: there is nothing to rob before house 0.

JavaScript
function rob(nums) {  const memo = new Map();  function best(i) {                         // most money from houses 0..i    if (i < 0) return 0;    if (memo.has(i)) return memo.get(i);    const answer = Math.max(best(i - 1), nums[i] + best(i - 2));    memo.set(i, answer);    return answer;  }  return best(nums.length - 1);}console.log(rob([1, 2, 3, 1]));    // 4console.log(rob([2, 7, 9, 3, 1])); // 12
How it works

The time and space are O(n). The state, the transition and the base case are written in the comments above. This has the same shape as stairs, but with a max instead of a sum.

Approach 2Bottom-up with two variables
The idea

Keep the best totals for the last two houses. Update them as you go along the row.

JavaScript
function rob(nums) {  let twoBack = 0, oneBack = 0;              // best(i-2) and best(i-1)  for (const money of nums) {    const here = Math.max(oneBack, twoBack + money);    twoBack = oneBack;    oneBack = here;  }  return oneBack;}console.log(rob([1, 2, 3, 1]));    // 4console.log(rob([2, 7, 9, 3, 1])); // 12console.log(rob([5]));             // 5
How it works

The time is O(n) and the space is O(1). Lesson 55 comes back to this problem and to the version where the houses are in a circle.

Which approach should you use?

Use bottom-up with two variables when you feel ready. The memo form makes the formula easiest to see. (LeetCode 198. It is a medium problem, shown here as a preview.)

Q6Pascal's triangleEasy

Given numRows, return the first numRows rows of Pascal's triangle. In this triangle, each number is the sum of the two numbers directly above it, and each row begins and ends with 1.

Example 1
Input: numRows = 5
Output: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
Explanation: Each inner number is the sum of the two numbers above it.
Example 2
Input: numRows = 1
Output: [[1]]
Explanation: It is just the top row.
Stuck? Open one hint at a time
Hint 1
Each row starts and ends with 1.
Hint 2
Every other number is the sum of the two numbers directly above it in the row before.
Hint 3
Row r depends only on row r − 1. So this is a table that you fill one row at a time.
Show all 2 solutions with explanations
Approach 1Build each row from the previous
The idea

Start with [[1]]. For each new row, begin and end with 1 and fill in the middle from the row above.

JavaScript
function generate(numRows) {  const triangle = [[1]];  for (let r = 1; r < numRows; r++) {    const prev = triangle[r - 1];    const row = [1];    for (let j = 1; j < r; j++) row.push(prev[j - 1] + prev[j]);    row.push(1);    triangle.push(row);  }  return triangle;}console.log(generate(5)); // [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]console.log(generate(1)); // [[1]]
How it works

The time and space are O(numRows²), because the output itself has that many numbers. The formula row[j] = prev[j-1] + prev[j] is a bottom-up DP table.

Approach 2Recursive cell with memo
The idea

Let cell(r, c) be the value in row r, column c. It is 1 on the two edges. Everywhere else it is the sum of the two cells above it. Save each cell in a cache.

JavaScript
function generate(numRows) {  const memo = new Map();  function cell(r, c) {    if (c === 0 || c === r) return 1;    const key = r + "," + c;    if (memo.has(key)) return memo.get(key);    const value = cell(r - 1, c - 1) + cell(r - 1, c);    memo.set(key, value);    return value;  }  const triangle = [];  for (let r = 0; r < numRows; r++) {    const row = [];    for (let c = 0; c <= r; c++) row.push(cell(r, c));    triangle.push(row);  }  return triangle;}console.log(generate(5)); // [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]console.log(generate(1)); // [[1]]
How it works

The state now has two parts, row and column, so the cache key joins them. The cost is the same, O(numRows²). Without the memo, cells would be worked out again an enormous number of times.

Which approach should you use?

The row-by-row table is simpler, and it is what interviewers expect. The memo version has a state with two values, which is a preview of 2-D DP (tables with rows and columns). (LeetCode 118.)

Q7Pascal's triangle IIEasy

Given rowIndex (counting from 0), return that row of Pascal's triangle. Try to use only O(rowIndex) extra space.

Example 1
Input: rowIndex = 3
Output: [1,3,3,1]
Explanation: This is row 3 (counting from 0).
Example 2
Input: rowIndex = 0
Output: [1]
Explanation: This is the top row.
Example 3
Input: rowIndex = 1
Output: [1,1]
Explanation: This is row 1.
Stuck? Open one hint at a time
Hint 1
You only need the last row, not the whole triangle. Can you change one array in place?
Hint 2
If you update from left to right, you overwrite a value you still need. Which direction avoids that?
Hint 3
Going from right to left, row[j] += row[j-1] uses the old values on its left.
Show all 2 solutions with explanations
Approach 1Build the full triangle
The idea

Generate all rows as in the previous question and return the last one.

JavaScript
function getRow(rowIndex) {  let prev = [1];  const all = [prev];  for (let r = 1; r <= rowIndex; r++) {    const row = [1];    for (let j = 1; j < r; j++) row.push(prev[j - 1] + prev[j]);    row.push(1);    all.push(row);    prev = row;  }  return all[rowIndex];}console.log(getRow(3)); // [ 1, 3, 3, 1 ]console.log(getRow(0)); // [ 1 ]console.log(getRow(1)); // [ 1, 1 ]
How it works

The time and space are O(rowIndex²), because it keeps every row even though only one is needed.

Approach 2One array, updated right to left
The idea

Start with [1]. To get to the next row, add a 1 at the end. Then, going from right to left, add each number's left neighbour to it.

JavaScript
function getRow(rowIndex) {  const row = [1];  for (let r = 1; r <= rowIndex; r++) {    row.push(1);                             // the new row is one longer    for (let j = r - 1; j >= 1; j--) {      row[j] += row[j - 1];                  // right to left: row[j-1] is still the old value    }  }  return row;}console.log(getRow(3)); // [ 1, 3, 3, 1 ]console.log(getRow(0)); // [ 1 ]console.log(getRow(1)); // [ 1, 1 ]
How it works

The time is O(rowIndex²), but the extra space is only O(rowIndex). This is the space-saving step from the lesson. Row r reads only row r − 1, so one array is enough, as long as you update in the safe direction.

Which approach should you use?

Use the single array. The direction of the inner loop is the whole lesson here. (LeetCode 119.)

Make it stick

Make it stick — close this page, then
  1. Use the recursion tree to explain why plain recursive Fibonacci is so slow (exponential).
  2. Name the two properties of a DP problem and give an example of each.
  3. List the four questions: state, transition, base cases, order.
  4. Turn a recursive function into a memoized one by adding a cache lookup and a cache write.
  5. Change the memoized version into a bottom-up table, then reduce it to O(1) space.
  6. Show with coins 1, 3, 4 and amount 6 why greedy fails but DP works.

Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.

What's next

In Lesson 55 you will practise this recipe on classic 1-D problems: house robber (take or skip), coin change, decode ways, word break and the longest increasing subsequence. In each one, the main work is choosing the state and the transition.

Practise on LeetCode

Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series