Skip to content

Expert: advanced patterns and interviews · Dynamic programming · topic 53 of 61 · 40 min

1-D DP classics

Explanation

Many interview DP problems use a one-dimensional table: dp[i] is the answer for the first i items, or for the amount i. House robber chooses between taking or skipping each house. Coin change finds the fewest coins for an amount, or counts the ways to make it, where the order of the two loops decides whether you count combinations or orderings. Decode ways and word break build a string answer one piece at a time.

House robber: take it or skip it

Houses stand in a row with some money in each. You may not take from two neighbouring houses (the alarm links them). What is the most you can collect? Greedy ideas such as "take every other house" fail: for [2, 1, 1, 9] the best is 2 + 9 = 11.

State: dp[i] = the most money from the first i houses. For house i − 1 (the last one in that group) there are two choices. Skip it: dp[i − 1]. Take it: then its neighbour must be skipped, so nums[i − 1] + dp[i − 2]. Recurrence: dp[i] = max(dp[i − 1], dp[i − 2] + nums[i − 1]), with dp[0] = 0 and dp[1] = nums[0]. Only two values are read, so two variables are enough: O(n) time, O(1) space.

houses so far[][2][2, 7][2, 7, 9][2, 7, 9, 3][2, 7, 9, 3, 1]
dp027111112

Coin change: the fewest coins

Given coin values (unlimited coins of each) and an amount, find the fewest coins that add up to the amount, or −1 if it cannot be done. Greedy ("always take the biggest coin that fits") works for Indian rupee coins, but fails for unusual sets: with coins [1, 3, 4] and amount 6, greedy takes 4 + 1 + 1 (three coins) while 3 + 3 uses two.

State: dp[a] = the fewest coins that make amount a. The last coin used was some c, so dp[a] = min over coins c ≤ a of dp[a − c] + 1. Base case: dp[0] = 0. Amounts that cannot be made stay at infinity, and you turn infinity into −1 at the end. Time O(amount × number of coins), space O(amount).

python
def coin_change_min(coins, amount):
    INF = float("inf")
    dp = [0] + [INF] * amount
    for a in range(1, amount + 1):
        for c in coins:
            if c <= a and dp[a - c] + 1 < dp[a]:
                dp[a] = dp[a - c] + 1
    return dp[amount] if dp[amount] != INF else -1

print(coin_change_min([1, 3, 4], 6))   # Output: 2
print(coin_change_min([2], 3))         # Output: -1

Coin change: counting the ways (loop order matters)

Now count how many different combinations of coins make the amount. With coins [1, 2] and amount 3 there are 2 combinations: 1+1+1 and 1+2. The order of coins does not matter, so 1+2 and 2+1 are the same combination.

State: ways[a] = the number of combinations that make a, with ways[0] = 1 (the empty combination). Put the coins in the OUTER loop and the amounts in the inner loop. Each coin is then "allowed" in a fixed order, so every combination is built in exactly one way: coins of the first kind, then of the second kind, and so on. If you swap the loops (amounts outside, coins inside), every ordering is counted separately and you get 3 for the example, because 1+2 and 2+1 both count. That other answer is right for a different question ("how many ordered sequences"), so read the question carefully.

python
def ways_combinations(coins, amount):
    ways = [1] + [0] * amount
    for c in coins:                      # coins outside
        for a in range(c, amount + 1):
            ways[a] += ways[a - c]
    return ways[amount]

def ways_orderings(coins, amount):
    ways = [1] + [0] * amount
    for a in range(1, amount + 1):       # amounts outside
        for c in coins:
            if c <= a:
                ways[a] += ways[a - c]
    return ways[amount]

print(ways_combinations([1, 2], 3), ways_orderings([1, 2], 3))   # Output: 2 3

Decode ways

A message of letters was turned into digits with A = 1, B = 2, … Z = 26. "226" could be "BZ" (2, 26), "VF" (22, 6) or "BBF" (2, 2, 6), so there are 3 ways to decode it. State: dp[i] = the number of ways to decode the first i characters, with dp[0] = 1. The last piece is either one digit (allowed if it is not "0") or two digits (allowed if they form 10 to 26). So dp[i] = (dp[i − 1] if s[i − 1] != "0") + (dp[i − 2] if 10 ≤ int(s[i − 2:i]) ≤ 26). Zeros are the trap: "06" has 0 ways, because "0" is not a letter and "06" is not allowed as a two-digit code.

Word break

Can the string be split into words from a dictionary? "applepenapple" with ["apple", "pen"] can; "catsandog" with ["cats", "dog", "sand", "and", "cat"] cannot. State: ok[i] = True if the first i characters can be split. ok[0] = True. ok[i] is True if some j < i has ok[j] True and s[j:i] in the word set. With n characters this checks O(n²) pairs, and each slice costs up to O(n), so O(n³) in the worst case; limiting j to the longest word length helps a lot.

python
def word_break(s, words):
    word_set = set(words)
    ok = [True] + [False] * len(s)
    for i in range(1, len(s) + 1):
        for j in range(i):
            if ok[j] and s[j:i] in word_set:
                ok[i] = True
                break
    return ok[len(s)]

print(word_break("applepenapple", ["apple", "pen"]))                     # Output: True
print(word_break("catsandog", ["cats", "dog", "sand", "and", "cat"]))   # Output: False

Common mistakes

  • House robber: assuming the answer is "every other house". Fix: write the take/skip recurrence and test [2, 1, 1, 9].
  • Coin change minimum: returning infinity, or 0, when the amount cannot be made. Fix: convert the "impossible" marker to −1 at the end, and test an impossible case.
  • Coin change ways: putting the loops in the wrong order and counting orderings. Fix: coins outside for combinations; test coins [1, 2] with amount 3, which must give 2.
  • Decode ways: forgetting that "0" cannot stand alone, or allowing "05" as a two-digit code. Fix: the one-digit piece needs s[i − 1] != "0"; the two-digit piece needs a value from 10 to 26.
  • Checking word break with a list of words instead of a set, making each lookup O(k). Fix: build a set once.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

1-D DP classics · 1/8

1-D DP classics

This lesson covers the one-dimensional DP classics that interviewers love: house robber, coin change, decode ways and word break.

Prefer a video by a teacher? Find free videos on “house robber coin change decode ways dynamic programming explained” on YouTube ↗

In real life: An ATM choosing notes

An ATM must pay out an amount with the notes it has loaded. With the usual note values, taking the largest note that fits works. But when some notes run out, the loaded set can be unusual. If a machine only has ₹200 and ₹500 notes and a customer asks for ₹600, greedy takes a ₹500 note and is stuck with ₹100, while three ₹200 notes work. A coin-change style DP finds the fewest notes, or tells the machine the amount is impossible so it can refuse at once. Real machines also track how many notes of each kind remain, which turns this into a bounded version of the problem.

python
def fewest_notes(notes, amount):
    INF = float("inf")
    dp = [0] + [INF] * amount
    for a in range(1, amount + 1):
        for n in notes:
            if n <= a:
                dp[a] = min(dp[a], dp[a - n] + 1)
    return dp[amount] if dp[amount] != INF else -1

print(fewest_notes([200, 500], 600))
print(fewest_notes([200, 500], 300))
Shows 3, then -1: three ₹200 notes make ₹600, and ₹300 cannot be paid.

Practice (0/4 done)

Task 1 · Code · Python

Write rob(nums) where nums[i] ≥ 0 is the money in house i along a street. You may not take money from two neighbouring houses. Return the largest total you can take. An empty street gives 0. O(n) time, O(1) extra space. Example: rob([2, 7, 9, 3, 1]) returns 12 (2 + 9 + 1).

Task 2 · Code · Python

Write coin_change_min(coins, amount) that returns the FEWEST coins that add up to amount, using unlimited coins of each value in coins (all positive). Return -1 if the amount cannot be made, and 0 for amount 0. Example: coin_change_min([1, 2, 5], 11) returns 3 (5 + 5 + 1).

Task 3 · Code · Python

Write coin_change_ways(coins, amount) that returns the number of different COMBINATIONS of coins (unlimited of each, all values distinct and positive) that add up to amount. Order does not matter: 1+2 and 2+1 are the same combination. amount 0 has exactly 1 combination (use no coins). Example: coin_change_ways([1, 2, 5], 5) returns 4. The starter has the loops in the wrong order.

Task 4 · Code · Python

A message of capital letters was encoded with A → "1", B → "2", …, Z → "26". Write num_decodings(s) that returns how many ways the digit string s can be decoded. "0" alone is not a letter, and a two-digit code must be between "10" and "26" (so "06" is not allowed). Return 0 for an empty string. Example: num_decodings("226") returns 3.

Quick quiz

Question 1

1.For house robber, what is dp[i] built from?

Question 2

2.Coins [1, 3, 4], amount 6. What does "always take the largest coin that fits" give, and what is optimal?

Question 3

3.In the ways-to-make-change DP, putting the AMOUNTS in the outer loop and coins in the inner loop counts…

Question 4

4.How many ways can "12" be decoded with A = 1 … Z = 26?

Question 5

5.What is the time of the coin-change-minimum DP with amount A and k coin values?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

For house robber, what is dp[i] built from?

Answer: max(dp[i − 1], dp[i − 2] + nums[i − 1])

Either skip the last house (dp[i − 1]) or take it and the best from two houses back.

Coins [1, 3, 4], amount 6. What does "always take the largest coin that fits" give, and what is optimal?

Answer: 3 coins and 2 coins

Greedy takes 4, 1, 1 (three coins); 3 + 3 needs only two. That is why DP is needed.

In the ways-to-make-change DP, putting the AMOUNTS in the outer loop and coins in the inner loop counts…

Answer: Ordered sequences, so 1+2 and 2+1 are counted separately

With amounts outside, every coin can be the last one at every amount, so each ordering is a separate path.

How many ways can "12" be decoded with A = 1 … Z = 26?

Answer: 2

"1 2" gives AB and "12" gives L.

What is the time of the coin-change-minimum DP with amount A and k coin values?

Answer: O(A × k)

There are A + 1 states, and each tries k coins.

Coins {1, 3, 4} and amount 6: how many coins do greedy and the best answer use?Related

Answer: Greedy 3, best 2

Greedy takes 4 + 1 + 1. The best is 3 + 3.

From Greedy algorithms: the idea

You only need the LCS LENGTH of two strings of lengths m and n. What is the smallest space you need with the usual DP?Related

Answer: O(min(m, n)) using two rows

Each row reads only the row above, so two rows of the shorter length are enough. To rebuild the string you need the full table (or a cleverer method).

From 2-D DP on grids and strings

A DP has n × m states and does O(1) work per state. What is its time?Related

Answer: O(n × m)

Time = number of states × work per state.

From Dynamic programming: the idea

nums = [1, 2, 3, 5]. Can it be split into two groups with equal sums?Related

Answer: No, the total 11 is odd

An odd total can never be split into two equal whole-number halves.

From Knapsack problems

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “Dynamic programming” before its exam: 2-D DP on grids and strings, Knapsack problems, More DP patterns and how to spot them.
  2. Module 11 exam: Dynamic programming · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Interview strategy and mock problems. 61 topics left in the course before the final exam.

← Previous: Dynamic programming: the idea