Solve 12 classic DP interview problems — maximum product subarray, decode ways, jump game II, unique paths with obstacles, minimum path sum, coin change II, target sum, longest palindromic substring, word break, stock problems, regular expression matching, and wildcard matching.
Problem 1 — Maximum Product Subarray
def max_product(nums):
"""
Find contiguous subarray with maximum product.
Track both max and min (negative × negative = positive!)
Time: O(n), Space: O(1)
"""
max_prod = min_prod = result = nums[0]
for num in nums[1:]:
candidates = (num, max_prod * num, min_prod * num)
max_prod = max(candidates)
min_prod = min(candidates)
result = max(result, max_prod)
return result
print(max_product([2, 3, -2, 4])) # 6 ([2,3])
print(max_product([-2, 0, -1])) # 0
print(max_product([-2, 3, -4])) # 24 ([-2,3,-4])
Problem 3 — Jump Game II (Minimum Jumps)
def jump_game_2(nums):
"""
Minimum jumps to reach last index.
Greedy DP: at each step, go as far as possible.
Time: O(n), Space: O(1)
"""
jumps = 0
current_end = 0 # Farthest position reachable with current jumps
farthest = 0 # Farthest position reachable overall
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == current_end: # We MUST jump here
jumps += 1
current_end = farthest
return jumps
print(jump_game_2([2,3,1,1,4])) # 2
print(jump_game_2([2,3,0,1,4])) # 2
Problem 4 — Unique Paths with Obstacles
def unique_paths_obstacles(grid):
"""
Count paths in grid with obstacles (1=blocked).
Only move right or down.
"""
m, n = len(grid), len(grid[0])
if grid[0][0] == 1 or grid[m-1][n-1] == 1: return 0
dp = [[0]*n for _ in range(m)]
dp[0][0] = 1
for i in range(m):
for j in range(n):
if i == 0 and j == 0: continue
if grid[i][j] == 1: continue # Obstacle
above = dp[i-1][j] if i > 0 else 0
left = dp[i][j-1] if j > 0 else 0
dp[i][j] = above + left
return dp[m-1][n-1]
grid = [[0,0,0],[0,1,0],[0,0,0]]
print(unique_paths_obstacles(grid)) # 2
Problem 5 — Coin Change II (Count Ways)
def change(amount, coins):
"""
Count number of ways to make amount using coins.
Each coin can be used unlimited times.
Unbounded knapsack variant.
"""
dp = [0] * (amount + 1)
dp[0] = 1 # One way to make 0: use no coins
for coin in coins:
for a in range(coin, amount+1): # Forward = unbounded
dp[a] += dp[a - coin]
return dp[amount]
print(change(5, [1,2,5])) # 4 ways
print(change(3, [2])) # 0 (impossible)
print(change(10, [10])) # 1
Problem 6 — Target Sum
def find_target_sum_ways(nums, target):
"""
Assign + or - to each number. Count ways to reach target.
DP: dp[sum] = number of ways to reach this sum.
"""
from collections import defaultdict
dp = defaultdict(int)
dp[0] = 1
for num in nums:
next_dp = defaultdict(int)
for curr_sum, count in dp.items():
next_dp[curr_sum + num] += count
next_dp[curr_sum - num] += count
dp = next_dp
return dp[target]
print(find_target_sum_ways([1,1,1,1,1], 3)) # 5
print(find_target_sum_ways([1], 1)) # 1
Problem 7 — Longest Palindromic Substring
def longest_palindrome(s):
"""
Find longest palindromic substring.
Expand around center: O(n²) time, O(1) space.
"""
start = end = 0
def expand(l, r):
while l >= 0 and r < len(s) and s[l] == s[r]:
l -= 1; r += 1
return l+1, r-1
for i in range(len(s)):
l1, r1 = expand(i, i) # Odd length
l2, r2 = expand(i, i+1) # Even length
if r1-l1 > end-start: start, end = l1, r1
if r2-l2 > end-start: start, end = l2, r2
return s[start:end+1]
print(longest_palindrome("babad")) # "bab" or "aba"
print(longest_palindrome("cbbd")) # "bb"
Problem 8 — Stock Buy and Sell with Cooldown
def max_profit_cooldown(prices):
"""
After selling, must wait 1 day (cooldown).
States: held (holding stock), sold (just sold), rest (cooling down or idle)
"""
held = float('-inf')
sold = 0
rest = 0
for price in prices:
prev_held, prev_sold, prev_rest = held, sold, rest
held = max(prev_held, prev_rest - price) # Keep or buy
sold = prev_held + price # Sell from held
rest = max(prev_rest, prev_sold) # Cool down or stay
return max(sold, rest)
print(max_profit_cooldown([1,2,3,0,2])) # 3 ([1→3] sell, cooldown, [0→2] buy→sell)
print(max_profit_cooldown([1])) # 0
Problem 9 — Stock with Transaction Fee
def max_profit_fee(prices, fee):
"""
Any number of transactions but each has a fee.
held: max profit while holding stock
cash: max profit without holding stock
"""
cash = 0
held = -prices[0]
for price in prices[1:]:
cash = max(cash, held + price - fee) # Sell
held = max(held, cash - price) # Buy
return cash
print(max_profit_fee([1,3,2,8,4,9], 2)) # 8
Problem 10 — Regular Expression Matching
def is_match_regex(s, p):
"""
'.' matches any single character.
'*' matches zero or more of the preceding element.
"""
m, n = len(s), len(p)
dp = [[False]*(n+1) for _ in range(m+1)]
dp[0][0] = True
# Initialize: pattern like a*, a*b*, a*b*c* can match empty string
for j in range(2, n+1):
if p[j-1] == '*':
dp[0][j] = dp[0][j-2]
for i in range(1, m+1):
for j in range(1, n+1):
if p[j-1] == '*':
# Use zero occurrences of preceding element
dp[i][j] = dp[i][j-2]
# Use one or more occurrences
if p[j-2] == '.' or p[j-2] == s[i-1]:
dp[i][j] = dp[i][j] or dp[i-1][j]
elif p[j-1] == '.' or p[j-1] == s[i-1]:
dp[i][j] = dp[i-1][j-1]
return dp[m][n]
print(is_match_regex("aa", "a")) # False
print(is_match_regex("aa", "a*")) # True
print(is_match_regex("ab", ".*")) # True
Problem 11 — Largest Rectangle in Histogram (DP + Stack)
def largest_rectangle(heights):
"""
Max rectangle area in histogram.
Monotonic stack: O(n) time.
"""
stack = []
max_area = 0
heights = heights + [0] # Sentinel to flush remaining bars
for i, h in enumerate(heights):
while stack and heights[stack[-1]] > h:
height = heights[stack.pop()]
width = i if not stack else i - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area
print(largest_rectangle([2,1,5,6,2,3])) # 10
print(largest_rectangle([2,4])) # 4
Problem 12 — Count Distinct Subsequences
def num_distinct(s, t):
"""
Count distinct subsequences of s that equal t.
dp[i][j] = ways to form t[:j] from s[:i]
"""
m, n = len(s), len(t)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(m+1): dp[i][0] = 1 # Empty t: one way (use nothing)
for i in range(1, m+1):
for j in range(1, n+1):
dp[i][j] = dp[i-1][j] # Don't use s[i-1]
if s[i-1] == t[j-1]:
dp[i][j] += dp[i-1][j-1] # Use s[i-1]
return dp[m][n]
print(num_distinct("rabbbit", "rabbit")) # 3
print(num_distinct("babgbag", "bag")) # 5
DP Pattern Recognition Guide
| Pattern in problem | DP Type | State definition |
|---|
| "Minimum steps/coins/edits" | 1D DP | dp[remaining] |
| "Count ways" | 1D/2D DP | dp[amount] or dp[i][j] |
| "Grid, only right/down" | 2D DP | dp[row][col] |
| "Two strings, compare" | 2D string DP | dp[i][j] for s1[:i],s2[:j] |
| "Subsequence/substring" | LCS/LIS variants | dp[i][j] or dp[i] |
| "Range [i..j]" | Interval DP | dp[i][j] |
| "Subsets" | Knapsack variants | dp[mask] or dp[sum] |
| "Stock trading" | State machine DP | held/sold/rest states |
*Dynamic Programming section complete.*