Expert: advanced patterns and interviews · Dynamic programming · topic 56 of 61 · 40 min
More DP patterns and how to spot them
Explanation
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).
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]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.
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
- Write the state in one sentence. Does every cell of your table match that sentence?
- Check the base cases by hand for the smallest inputs (empty, one item, amount 0).
- Print the table for a tiny example and compare it with a table you fill on paper.
- Check the filling order: is every cell you read already final?
- Check the loop direction (0/1 versus unbounded) and the loop order (combinations versus orderings).
- 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
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.
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))Practice (0/4 done)
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.
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.
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).
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- 1-D DP classicsDynamic programming · same module
- Recognising the patternInterview strategy and mock problems
- Sliding window: variable sizeHashing, two pointers and sliding window
- Core DSA patterns in C++C++ Programming (modern C++ and DSA) · Competitive programming, interviews and careers
- Interval problemsHeaps and greedy algorithms
- Testing, edge cases and debugging under pressureInterview strategy and mock problems
Questions people ask about this
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).
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).
"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.
A DP has n × m states and does O(1) work per state. What is its time?Related
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
- This is the last topic of “Dynamic programming”. The module exam comes next.
- Module 11 exam: Dynamic programming · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Interview strategy and mock problems. 61 topics left in the course before the final exam.