Skip to content

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

More DP patterns and how to spot them

Explanation

A few more patterns cover most of the DP you will meet in interviews: the longest increasing subsequence (O(n²) DP, or O(n log n) with binary search), the maximum product subarray (track both the largest and the smallest), and palindromic substrings (expand around each centre). Interval DP and bitmask DP handle ranges and small sets. Learn the signals in a question that point to DP, and a checklist for debugging a table that gives wrong answers.

Longest increasing subsequence (LIS)

Given a list of numbers, find the length of the longest subsequence (gaps allowed, order kept) whose numbers strictly increase. For [10, 9, 2, 5, 3, 7, 101, 18] one answer is [2, 3, 7, 18], length 4.

The O(n²) DP: dp[i] = the length of the longest increasing subsequence that ENDS at index i. It is 1 (just nums[i]) or 1 + dp[j] for any earlier j with nums[j] < nums[i]. The answer is max(dp). Two nested loops give O(n²) time and O(n) space, fine for n up to a few thousand.

The O(n log n) idea keeps a list called tails, where tails[k] is the SMALLEST possible last number of an increasing subsequence of length k + 1. A smaller tail is always better, because more numbers can extend it later. tails stays sorted, so for each x use binary search (bisect_left) to find the first tail ≥ x. If there is none, x extends the longest subsequence: append it. Otherwise replace that tail with x. The answer is len(tails).

python
from bisect import bisect_left

def length_of_lis(nums):
    tails = []
    for x in nums:
        i = bisect_left(tails, x)
        if i == len(tails):
            tails.append(x)
        else:
            tails[i] = x
    return len(tails)

print(length_of_lis([10, 9, 2, 5, 3, 7, 101, 18]))   # Output: 4
# tails after each number: [10] [9] [2] [2, 5] [2, 3] [2, 3, 7] [2, 3, 7, 101] [2, 3, 7, 18]
O(n log n) time, O(n) space. Use bisect_right instead if equal numbers are allowed (non-decreasing).
Careful: tails is not always a real subsequence of the input; only its LENGTH is guaranteed. To print an actual LIS, also store, for each number, the index of the number before it, and walk back.

Maximum product subarray

Find the largest product of a run of neighbouring numbers. Kadane's idea from Module 2 needs a twist: a negative number turns the smallest (most negative) product into the largest. So keep two values for the run ending here: hi (largest product) and lo (smallest product). For each x, the candidates are x, hi × x and lo × x; the new hi is their max and the new lo is their min. A zero resets both to 0, and the next number starts fresh. O(n) time, O(1) space. For [2, 3, −2, 4] the answer is 6; for [−2, 3, −4] it is 24.

Palindromic substrings

Every palindrome has a centre: one character (odd length, like "aba") or the gap between two characters (even length, like "abba"). A string of length n has 2n − 1 centres. From each centre, expand outwards while the two ends match. Each expansion step finds one more palindrome, so counting all palindromic substrings and finding the longest one both take O(n²) time and O(1) extra space.

python
def count_palindromes(s):
    count = 0
    for centre in range(2 * len(s) - 1):
        left = centre // 2
        right = left + centre % 2
        while left >= 0 and right < len(s) and s[left] == s[right]:
            count += 1
            left -= 1
            right += 1
    return count

print(count_palindromes("aaa"))   # Output: 6  (a, a, a, aa, aa, aaa)

The DP version uses a table pal[i][j] = True if s[i..j] is a palindrome: s[i] == s[j] and (j − i < 2 or pal[i + 1][j − 1]). Fill it by increasing length. It is also O(n²) time, but needs O(n²) space, so expanding around centres is usually preferred in interviews.

Interval DP and bitmask DP in brief

Interval DP: the state is a range [i, j] of the input, and the answer for a range is built by trying every split point k between i and j. Examples are matrix-chain multiplication and "burst balloons". Fill the table by increasing range length so smaller ranges are ready first. There are O(n²) ranges with O(n) splits each, so O(n³) time.

Bitmask DP: the state includes a SET of items already used, stored as the bits of an integer (Module 10). The classic is the travelling salesman problem on up to about 20 cities: dp[mask][last] = the cheapest way to visit exactly the cities in mask, ending at last. That is O(2ⁿ × n²) time, so it only fits when n is small, which the constraints will tell you.

How to recognise a DP problem

  • The question asks for a COUNT ("how many ways"), a best value ("minimum cost", "maximum profit", "longest"), or a yes/no ("can we reach", "is it possible").
  • At each step you make a choice (take or skip, which coin, which move), and later choices depend on earlier ones.
  • A brute-force recursion would call itself with the same arguments many times.
  • The input is a sequence, a string, a grid, or an amount, and the answer for a prefix or a smaller amount helps with the whole.
  • Greedy ideas fail on a small counter-example you can find in a minute.

A debugging checklist

  1. Write the state in one sentence. Does every cell of your table match that sentence?
  2. Check the base cases by hand for the smallest inputs (empty, one item, amount 0).
  3. Print the table for a tiny example and compare it with a table you fill on paper.
  4. Check the filling order: is every cell you read already final?
  5. Check the loop direction (0/1 versus unbounded) and the loop order (combinations versus orderings).
  6. Check the answer cell: dp[n], dp[n][m], max(dp), or something else?

Common mistakes

  • Using bisect_right for a STRICTLY increasing LIS, which lets equal numbers extend the sequence. Fix: bisect_left for strict, bisect_right for non-decreasing.
  • Tracking only the maximum in max product. Fix: track the minimum too, and compute the new hi and lo from the OLD values at the same time.
  • Expanding only odd-length centres and missing "abba". Fix: try both centres (i, i) and (i, i + 1).
  • Answering with dp[n − 1] in LIS when the best subsequence ends earlier. Fix: in the O(n²) version the answer is max(dp).

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

More DP patterns and how to spot them · 1/9

More DP patterns

This lesson adds the last common DP patterns, and teaches you how to spot DP in a new question.

Prefer a video by a teacher? Find free videos on “longest increasing subsequence n log n maximum product subarray longest palindromic substring” on YouTube ↗

In real life: Nesting delivery boxes

A packaging team wants to nest boxes inside each other to save space in a warehouse; a box fits inside another only if it is strictly smaller in both width and height. Sort the boxes by width (and, for equal widths, by height from largest to smallest so two boxes of the same width are never chained), then find the longest strictly increasing subsequence of the heights. This is the "Russian doll envelopes" interview problem, and the O(n log n) LIS makes it fast even for many boxes.

python
from bisect import bisect_left

boxes = [(5, 4), (6, 4), (6, 7), (2, 3), (8, 9), (8, 5)]   # (width, height)
boxes.sort(key=lambda b: (b[0], -b[1]))
tails = []
for w, h in boxes:
    i = bisect_left(tails, h)
    if i == len(tails):
        tails.append(h)
    else:
        tails[i] = h
print(len(tails))
Shows 4: (2, 3) inside (5, 4) inside (6, 7) inside (8, 9).

Practice (0/4 done)

Task 1 · Code · Python

Write length_of_lis(nums) that returns the length of the longest STRICTLY increasing subsequence of nums (gaps allowed, order kept). Use the tails list with bisect for O(n log n) time. Return 0 for an empty list. Example: length_of_lis([10, 9, 2, 5, 3, 7, 101, 18]) returns 4.

Task 2 · Code · Python

Write max_product(nums) that returns the largest product of a non-empty run of neighbouring numbers. nums has at least one integer and may contain negatives and zeros. O(n) time, O(1) extra space. Example: max_product([2, 3, -2, 4]) returns 6.

Task 3 · Code · Python

Write longest_palindrome(s) that returns the longest substring of s (continuous) that reads the same forwards and backwards. If several have the same maximum length, return the one that STARTS FIRST (smallest index). Return "" for an empty string. Use expand-around-centre: O(n²) time, O(1) extra space. Example: longest_palindrome("babad") returns "bab" (not "aba", which starts later).

Task 4 · Code · Javascript

In JavaScript, write countPalindromicSubstrings(s) that returns how many substrings of s are palindromes. Substrings at different positions count separately, even if they look the same. Use expand-around-centre (O(n²) time). Example: countPalindromicSubstrings("aaa") returns 6 (a, a, a, aa, aa, aaa). An empty string gives 0.

Quick quiz

Question 1

1.In the O(n log n) LIS, what does tails[k] hold?

Question 2

2.Why does maximum product subarray track the minimum product too?

Question 3

3.How many centres does expand-around-centre try for a string of length n?

Question 4

4.A question says "n ≤ 15 tasks, assign each worker a different task, minimise the total cost". Which DP pattern fits?

Question 5

5.Which question wording is the weakest signal for DP?

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.

In the O(n log n) LIS, what does tails[k] hold?

Answer: The smallest possible last number of an increasing subsequence of length k + 1

Keeping the smallest tail for each length leaves the most room for later numbers to extend it.

Why does maximum product subarray track the minimum product too?

Answer: Because multiplying the most negative product by a negative number can give the largest product

For [−2, 3, −4], the run −2 × 3 = −6 is the minimum, and −6 × −4 = 24 is the answer.

How many centres does expand-around-centre try for a string of length n?

Answer: 2n − 1

n single-character centres plus n − 1 gaps between neighbours.

A question says "n ≤ 15 tasks, assign each worker a different task, minimise the total cost". Which DP pattern fits?

Answer: Bitmask DP over the set of used tasks

The state needs to know which tasks are already used; with n ≤ 15 there are only 2¹⁵ = 32,768 sets.

Which question wording is the weakest signal for DP?

Answer: "Print all the items in reverse order"

Reversing is a single pass; the other three ask for a count, an optimum or a yes/no built from choices.

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

Why is max(prices) − min(prices) wrong for the one-trade stock problem?Related

Answer: The maximum may come BEFORE the minimum, which would mean selling before buying

For [9, 1, 2] it gives 8, but the real best is 1 (buy at 1, sell at 2).

From Maximum subarray: Kadane's algorithm

"Find the length of the longest substring with at most k distinct characters." Which pattern?Related

Answer: Sliding window with a count map

A substring (continuous) with a condition that gets worse as it grows: grow the right end, shrink the left end.

From Recognising the pattern

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

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. This is the last topic of “Dynamic programming”. The module exam comes next.
  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: Knapsack problems