AI SWE Prep
0/23in Data Structures & Algorithms
Data Structures & Algorithms

Dynamic Programming I: 1-D

Overlapping subproblems and optimal substructure. Build up from memoized recursion to clean bottom-up tables.

~18 minLesson 31 of 60
Your progressNot started

Dynamic programming is what you use when brute-force recursion keeps solving the same smaller question. Instead of recomputing, store the answer to each subproblem once and reuse it. The speedup can be dramatic because a recursion tree collapses into a table.

The two properties you need are overlapping subproblems and optimal substructure. Overlap means the same state appears many times. Optimal substructure means the answer to a larger state can be built from answers to smaller states.

In 1-D DP, the state is usually one number: an index, an amount, a length, or a remaining capacity. The whole craft is defining that state so the recurrence becomes obvious.

The mental model

Build DP in this order:

  1. State. What does dp[i] mean in one sentence?
  2. Transition. Which smaller states produce dp[i]?
  3. Base cases. What values are known without recursion?
  4. Fill order. Do the smaller states exist before you read them?
  5. Answer. Is it dp[n], max(dp), or something else?

The same recurrence can be written three ways. Start with recursion for clarity, add memoization to avoid repeats, then turn it into tabulation when the order is simple:

from functools import cache

@cache
def ways(i):
    if i <= 1:
        return 1
    return ways(i - 1) + ways(i - 2)

Bottom-up writes the states from small to large:

def ways(n):
    dp = [0] * (n + 1)
    dp[0] = dp[1] = 1

    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]

    return dp[n]

If each state only depends on the previous one or two states, you can drop the array and keep rolling variables. Do this after the recurrence is correct, not before.

1-D DP · climbing stairsStep 1 / 8
n=01
n=11
n=2?
n=3?
n=4?
n=5?
n=6?
n=7?
n=8?
Base cases: 1 way to reach step 0 (stand still) and step 1.
just computedsummed fromEach state depends only on the previous two — so O(1) memory suffices.

Pattern recognition

Variations

Worked problems

Try each yourself before revealing the solution. Notice how the state definition changes, but the method stays the same.

LeetCode 70Easy
  • DP
  • Counting
  • Rolling state

Climbing Stairs

You can climb either 1 or 2 steps at a time. Given n steps, return how many distinct ways there are to reach the top.

Approach. Let dp[i] be the number of ways to stand on step i. The last move was either from i - 1 or i - 2, so dp[i] = dp[i - 1] + dp[i - 2]. Since only two previous values are needed, keep rolling variables instead of a full table.

Show solution
def climb_stairs(n):
    if n <= 2:
        return n

    two_back = 1
    one_back = 2

    for _ in range(3, n + 1):
        two_back, one_back = one_back, one_back + two_back

    return one_back

Complexity. O(n) time and O(1) space. This is Fibonacci with different base cases.

LeetCode 198Medium
  • DP
  • Choose skip
  • Rolling state

House Robber

A row of houses contains some amount of money in each house. You may not rob two adjacent houses. Return the maximum amount you can collect.

Approach. At each house, choose between skipping it and keeping the best total so far, or taking it and adding its money to the best total from two houses back. The recurrence is best[i] = max(best[i - 1], best[i - 2] + nums[i]).

Show solution
def rob(nums):
    two_back = 0
    one_back = 0

    for money in nums:
        current = max(one_back, two_back + money)
        two_back, one_back = one_back, current

    return one_back

Complexity. O(n) time and O(1) space. The rolling variables store the only two states the next house can depend on.

LeetCode 322Medium
  • DP
  • Minimization
  • Unbounded

Coin Change

Given coin denominations and a target amount, return the fewest coins needed to make that amount. If the amount cannot be formed, return -1.

Approach. Let dp[total] be the minimum number of coins needed for total. For each total, try every coin as the last coin used. If total - coin is possible, then dp[total - coin] + 1 is a candidate answer.

Show solution
def coin_change(coins, amount):
    dp = [float("inf")] * (amount + 1)
    dp[0] = 0

    for total in range(1, amount + 1):
        for coin in coins:
            if coin <= total:
                dp[total] = min(dp[total], dp[total - coin] + 1)

    return -1 if dp[amount] == float("inf") else dp[amount]

Complexity. O(amount · c) time and O(amount) space, where c is the number of coin denominations. Every amount considers every coin once.

LeetCode 300Medium
  • DP
  • Sequence

Longest Increasing Subsequence

Given an integer array, return the length of the longest subsequence whose values strictly increase. The subsequence does not need to be contiguous.

Approach. Let dp[i] be the length of the longest increasing subsequence that ends exactly at index i. To extend into i, choose a previous index j with nums[j] < nums[i], then append nums[i] to the best subsequence ending at j. The final answer is max(dp), not necessarily dp[-1].

Show solution
def length_of_lis(nums):
    if not nums:
        return 0

    dp = [1] * len(nums)

    for i in range(len(nums)):
        for j in range(i):
            if nums[j] < nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)

    return max(dp)

Complexity. O(n²) time and O(n) space. There is an O(n log n) patience-sorting optimization, but this DP recurrence is the foundation.

The state-definition pitfall

DP is overkill when every subproblem is unique, and it is unnecessary when a greedy invariant proves one choice dominates the rest. Use it when the search tree repeats itself and a compact state can remember the only history that matters.

Your progressNot started