{}DSA Atlas

1D Dynamic Programming

Linear DP over a sequence — take/skip decisions, decoding and partitioning, longest increasing subsequence, word break, and state-machine DP for stock problems.

Intermediate14 practice problems12 medium2 hard

Shape of the problems

The input is a single sequence (array, string, or a number line up to n), and dp[i] summarizes the first i elements or the subsequence ending at i. Transitions look back a constant number of steps (O(n) total) or at all earlier indices (O(n²)).

Family 1: Take or skip (House Robber)

def rob(nums):
    prev2 = prev1 = 0              # best up to i-2, best up to i-1
    for x in nums:
        prev2, prev1 = prev1, max(prev1, prev2 + x)
    return prev1
int rob(vector<int>& nums) {
    int prev2 = 0, prev1 = 0;
    for (int x : nums) {
        int cur = max(prev1, prev2 + x);
        prev2 = prev1; prev1 = cur;
    }
    return prev1;
}

Transformations to House Robber:

  • Circular street: run it twice — without the first house and without the last.
  • Delete and Earn: taking value v forbids v−1 and v+1 → sum points per value, then rob over the value line.
  • Tree-shaped street: tree DP.

Family 2: Counting ways to decode / partition

dp[i] = number of ways to handle the prefix of length i. Look back one and two characters.

Python
def num_decodings(s):
    n = len(s)
    dp = [0] * (n + 1)
    dp[0] = 1                                   # empty prefix: one way
    for i in range(1, n + 1):
        if s[i - 1] != "0":
            dp[i] += dp[i - 1]                  # single digit
        if i >= 2 and 10 <= int(s[i - 2:i]) <= 26:
            dp[i] += dp[i - 2]                  # two digits
    return dp[n]

Word break: look back at every possible last word

def word_break(s, word_dict):
    words = set(word_dict)
    max_len = max(map(len, words), default=0)
    n = len(s)
    dp = [False] * (n + 1)
    dp[0] = True
    for i in range(1, n + 1):
        for j in range(max(0, i - max_len), i):
            if dp[j] and s[j:i] in words:
                dp[i] = True
                break
    return dp[n]
bool wordBreak(string s, vector<string>& wordDict) {
    unordered_set<string> words(wordDict.begin(), wordDict.end());
    int n = s.size(), maxLen = 0;
    for (auto& w : wordDict) maxLen = max(maxLen, (int)w.size());
    vector<bool> dp(n + 1, false);
    dp[0] = true;
    for (int i = 1; i <= n; i++)
        for (int j = max(0, i - maxLen); j < i && !dp[i]; j++)
            if (dp[j] && words.count(s.substr(j, i - j))) dp[i] = true;
    return dp[n];
}

The general shape — dp[i] = combine over j < i of dp[j] ⊕ cost(j, i) — is partition DP. Other examples: Palindrome Partitioning II (min cuts), Perfect Squares, Minimum Cost For Tickets.

Family 3: Longest increasing subsequence (LIS)

A subsequence keeps order but may skip elements.

O(n²) — “ending at i”

Python
def length_of_lis(nums):
    n = len(nums)
    dp = [1] * n                     # LIS ending exactly at i
    for i in range(n):
        for j in range(i):
            if nums[j] < nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp, default=0)

O(n log n) — patience sorting

Keep tails[k] = the smallest possible tail of an increasing subsequence of length k + 1. tails is sorted, so each new element binary-searches its position.

from bisect import bisect_left

def length_of_lis(nums):
    tails = []
    for x in nums:
        i = bisect_left(tails, x)    # use bisect_right for non-decreasing
        if i == len(tails):
            tails.append(x)
        else:
            tails[i] = x             # a smaller tail for length i+1
    return len(tails)
int lengthOfLIS(vector<int>& nums) {
    vector<int> tails;
    for (int x : nums) {
        auto it = lower_bound(tails.begin(), tails.end(), x);
        if (it == tails.end()) tails.push_back(x);
        else *it = x;
    }
    return tails.size();
}

LIS in disguise:

  • Russian Doll Envelopes: sort by width ↑ and height ↓ (so equal widths can't nest), then LIS on heights.
  • Maximum Length of Pair Chain: LIS on intervals (or greedy by end).
  • Minimum deletions to make sorted: n − LIS.
  • Longest Bitonic Subsequence: LIS from the left + LIS from the right.
  • Number of LIS: keep (length, count) pairs.

Family 4: Kadane-style “best ending here”

Maximum subarray and maximum product subarray are 1D DPs with O(1) state. See Arrays.

Python
def max_product(nums):
    hi = lo = best = nums[0]
    for x in nums[1:]:
        candidates = (x, hi * x, lo * x)
        hi, lo = max(candidates), min(candidates)
        best = max(best, hi)
    return best

Family 5: State machines (stock problems)

When each day you're in one of a few statuses (holding a stock or not, in cooldown, transactions used), keep one DP value per status and write transitions between them.

          buy                 sell
  ┌─────┐ ─────►  ┌──────┐  ─────►  ┌──────┐
  │ rest│         │ hold │          │ sold │
  └─────┘ ◄───────└──────┘          └──────┘
     ▲      (stay)            cooldown  │
     └──────────────────────────────────┘
def max_profit_cooldown(prices):
    hold, sold, rest = float("-inf"), 0, 0
    for p in prices:
        hold, sold, rest = (
            max(hold, rest - p),     # keep holding, or buy (only from rest)
            hold + p,                # sell today
            max(rest, sold),         # do nothing; yesterday's sale ends cooldown
        )
    return max(sold, rest)
int maxProfit(vector<int>& prices) {
    long long hold = LLONG_MIN / 2, sold = 0, rest = 0;
    for (int p : prices) {
        long long h = max(hold, rest - p), s = hold + p, r = max(rest, sold);
        hold = h; sold = s; rest = r;
    }
    return max(sold, rest);
}

The whole stock family:

VariantStates
one transactionmin price so far
unlimitedcash, hold
with feecash = max(cash, hold + p − fee), hold = max(hold, cash − p)
with cooldownhold, sold, rest
at most k transactionsbuy[j], sell[j] for j = 1..k
Python
def max_profit_k(k, prices):
    buy = [float("-inf")] * (k + 1)
    sell = [0] * (k + 1)
    for p in prices:
        for j in range(1, k + 1):
            buy[j] = max(buy[j], sell[j - 1] - p)
            sell[j] = max(sell[j], buy[j] + p)
    return sell[k]

State machines also model Paint House (last color), Domino Tiling (profile of the last column), and Student Attendance Record (absences so far, trailing lates).

Choosing the look-back

Transition looks atComplexityExample
constant number of previous statesO(n)robber, decode ways, stock
all previous jO(n²)LIS, word break, partition DP
previous j within a windowO(n) with monotonic dequeJump Game VI, Constrained Subsequence Sum
previous j by valueO(n log n) with BIT/segment tree or binary searchLIS, Longest Increasing Subsequence II

Common mistakes

  • Confusing “ending at i” with “among first i” and returning dp[n−1] instead of max(dp).
  • Leading zeros in decode ways ("06" is not valid).
  • Using bisect_right vs bisect_left incorrectly for strict vs non-strict LIS.
  • Stock DP: updating states in place so today's buy feeds today's sell (use temporaries or tuple assignment).

Practice problems

Ordered roughly from warm-up to hard. Try each one for 25–40 minutes before peeking at the key idea; if you needed the hint, mark it “review” and redo it in a few days.

0 / 14 solved
rob(i) = max(rob(i−1), rob(i−2) + nums[i]).
Medium
Circular: max(rob(nums[1:]), rob(nums[:-1])).
Medium
dp[i] += dp[i−1] if s[i−1] != '0'; dp[i] += dp[i−2] if 10 <= s[i−2..i−1] <= 26.
Medium
dp[i] = any(dp[j] and s[j:i] in words) — limit j by the max word length.
Medium
O(n^2): dp[i] = 1 + max(dp[j] for j < i with a[j] < a[i]). O(n log n): tails + bisect.
Medium
Track max and min product ending here.
Medium
States hold/sold/rest; sold today → rest tomorrow.
Medium
cash = max(cash, hold + p − fee); hold = max(hold, cash − p).
Medium
Bucket totals by value, then House Robber over values.
Medium
Track (length, count) per index; equal lengths add counts.
Medium
DP works in O(n^2); greedy BFS levels in O(n).
Medium
Sort width ascending, height DESCENDING for equal widths; LIS on heights in O(n log n).
Hard
buy[j], sell[j] for j transactions; if k >= n/2 it's unlimited.
Hard
dp[day] = min(dp[day−1]+c1, dp[day−7]+c7, dp[day−30]+c30) on travel days; dp[day] = dp[day−1] otherwise.
Medium

Click the box to cycle: solved ✓ → needs review ↺ → not started. Links open LeetCode.