93 Memoize decorator 10s

lru_cache converts top-down recursion to DP. Eliminates manual memo dict boilerplate.

from functools import lru_cache

@lru_cache(maxsize=None)
def solve(n):
    ...
94 1D tabulation template 10s

Bottom-up 1D DP builds solutions iteratively. Avoids recursion depth limits.

dp = [0] * (n + 1)
for i in range(1, n + 1):
    dp[i] = ...  # recurrence
95 2D DP grid init 10s

2D DP grid handles two-dimensional state. Foundation for LCS, edit distance, and grid paths.

dp = [[0] * (m + 1) for _ in range(n + 1)]
96 Fibonacci DP 10s

Fibonacci DP is the simplest recurrence. Same pattern as climbing stairs.

dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
    dp[i] = dp[i - 1] + dp[i - 2]
97 Coin change template 15s

Unbounded knapsack DP: iterate coins outer, amounts inner. Classic DP problem.

dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins:
    for x in range(coin, amount + 1):
        dp[x] = min(dp[x], dp[x - coin] + 1)
98 Subset generation 15s

Subset backtracking explores include/exclude decisions. Foundation for power set and combination sum.

def bt(i, cur):
    res.append(cur[:])
    for j in range(i, n):
        cur.append(a[j])
        bt(j + 1, cur)
        cur.pop()
bt(0, [])
99 Permutation template 15s

Permutation backtracking generates all orderings. Used in arrangement and scheduling problems.

def bt(cur):
    if len(cur) == n:
        res.append(cur[:])
        return
    for x in a:
        if x not in cur:
            cur.append(x)
            bt(cur)
            cur.pop()
bt([])
100 Combination template 15s

Combination backtracking selects k items from n. Avoids duplicates by advancing the start index.

def bt(start, cur):
    if len(cur) == k:
        res.append(cur[:])
        return
    for i in range(start, n):
        cur.append(a[i])
        bt(i + 1, cur)
        cur.pop()
bt(0, [])
127 Unique paths grid DP 10s

1D DP accumulation for grid paths. Space-optimized from 2D.

dp = [1] * n
for _ in range(1, m):
    for j in range(1, n):
        dp[j] += dp[j-1]
128 LCS bottom-up 15s

Longest Common Subsequence is a classic 2D DP. Foundation for diff algorithms.

dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
    for j in range(1, n+1):
        if a[i-1] == b[j-1]:
            dp[i][j] = dp[i-1][j-1] + 1
        else:
            dp[i][j] = max(dp[i-1][j], dp[i][j-1])
129 Edit distance recurrence 10s

Edit distance uses three operations: insert, delete, replace. Each maps to a DP neighbor.

if a[i-1] == b[j-1]:
    dp[i][j] = dp[i-1][j-1]
else:
    dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
130 Coin change 2 (ways) 10s

Counting coin combinations iterates coins outer to avoid permutation double-counting.

dp = [0] * (amount + 1)
dp[0] = 1
for c in coins:
    for a in range(c, amount + 1):
        dp[a] += dp[a - c]
131 Target sum DP 15s

Transform target sum into a subset-sum problem via (total + target) / 2.

total = sum(nums)
if (total + target) % 2: return 0
s = (total + target) // 2
dp = [0] * (s + 1)
dp[0] = 1
for n in nums:
    for j in range(s, n-1, -1):
        dp[j] += dp[j - n]
132 Interleaving string check 15s

2D DP checks if characters from s1 and s2 interleave to form s3.

dp[0][0] = True
for i in range(m+1):
    for j in range(n+1):
        if i and s1[i-1] == s3[i+j-1]: dp[i][j] |= dp[i-1][j]
        if j and s2[j-1] == s3[i+j-1]: dp[i][j] |= dp[i][j-1]
136 Generate parentheses backtrack 15s

Track open/close counts. Only add close paren when close < open.

def bt(o, c, cur):
    if len(cur) == 2*n: res.append(cur); return
    if o &lt; n: bt(o+1, c, cur+'(')
    if c &lt; o: bt(o, c+1, cur+')')
151 How to recognize a DP problem 15s

Before writing any DP code, verify both conditions: overlapping subproblems AND optimal substructure. If only one holds, DP won't help.

RECOGNIZING A DP PROBLEM — CHECKLIST
─────────────────────────────────────
1. OVERLAPPING SUBPROBLEMS?
   → Same function called with same args multiple times
   → Draw the recursion tree: do branches repeat?

2. OPTIMAL SUBSTRUCTURE?
   → Optimal solution built from optimal sub-solutions
   → "Best of X" = best(sub1) + best(sub2)?

3. CLASSIC SIGNALS in problem statement:
   □ "minimum cost / maximum profit"
   □ "number of ways to..."
   □ "can you reach / is it possible"
   □ "longest / shortest subsequence"
   □ "partition into groups optimally"

4. NOT DP if:
   × Need the actual path, not just cost → maybe BFS
   × Greedy choice property holds → greedy
   × No overlapping subproblems → plain recursion
152 Top-down vs bottom-up tradeoffs 15s

Top-down is easier to write and debug. Bottom-up avoids stack overflow and is faster. Know both, convert between them.

TOP-DOWN (memoization)          BOTTOM-UP (tabulation)
─────────────────────           ──────────────────────
+ Natural recursive thinking    + No recursion stack limit
+ Only computes needed states   + Often cache-friendly
- Python recursion limit        - Must fill entire table
- Stack overhead                - Must figure iteration order

CONVERSION STEPS:
  Top-down → Bottom-up:
  1. Identify base cases → fill dp table edges
  2. Reverse recursion → forward iteration
  3. memo dict → dp array

  Bottom-up → Space-optimized:
  1. Check: does dp[i] only depend on dp[i-1]?
  2. Yes → keep only prev row/value
  3. Two rows → rolling array

Rule: start top-down for correctness,
      convert to bottom-up if perf matters.
153 DP state definition checklist 15s

Defining the state is the hardest part of DP. Get this right and the recurrence writes itself.

DP STATE DEFINITION — "WHAT CHANGES?"
──────────────────────────────────────
Ask: what variables change between subproblems?

COMMON STATE PATTERNS:
  1D: dp[i] = answer for first i items
      → climbing stairs, house robber, word break

  2D: dp[i][j] = answer for items i..j  (intervals)
      → palindrome, matrix chain, burst balloons
      OR dp[i][j] = answer using i items with capacity j
      → knapsack, edit distance, LCS

  State machine: dp[i][state]
      → stock buy/sell (hold/sold/rest)
      → house robber circular

  Bitmask: dp[mask] = answer for subset represented by mask
      → TSP, partition to K subsets

CHECKLIST:
  □ What is the "answer" stored in dp[...]?
  □ What are the dimensions? (index, capacity, state)
  □ What are the transitions? dp[i] = f(dp[i-1], ...)
  □ What are the base cases?
154 Space optimization: rolling array 15s

Most 2D DP can be reduced to 1D. Most 1D DP can be reduced to a few variables. Always check dependencies.

SPACE OPTIMIZATION PATTERNS
────────────────────────────
# 2D → 1D (when row i only depends on row i-1):
# BEFORE: O(n*m) space
dp = [[0]*(m+1) for _ in range(n+1)]
for i in range(1, n+1):
    for j in range(1, m+1):
        dp[i][j] = dp[i-1][j-1] + 1  # example

# AFTER: O(m) space — two rows
prev, curr = [0]*(m+1), [0]*(m+1)
for i in range(1, n+1):
    for j in range(1, m+1):
        curr[j] = prev[j-1] + 1
    prev, curr = curr, [0]*(m+1)

# 1D → O(1) (when dp[i] only needs dp[i-1], dp[i-2]):
# BEFORE:
dp = [0] * n
# AFTER:
a, b = base0, base1
for i in range(2, n):
    a, b = b, f(a, b)

KEY: iterate inner loop BACKWARDS for 0/1 knapsack
     to avoid using updated values.
155 1D vs 2D DP: how to decide 15s

Counting how many variables change between subproblems immediately tells you the DP table dimensionality.

1D vs 2D DP — DECISION TREE
────────────────────────────
Q: How many things change between subproblems?

ONE THING changes → 1D dp[i]
  "first i elements" or "up to value i"
  Examples: climbing stairs, house robber,
            word break, decode ways, coin change

TWO THINGS change → 2D dp[i][j]
  Patterns:
  ┌─ Two sequences → dp[i][j] = first i of seq1, j of seq2
  │  LCS, edit distance, regex matching
  ├─ Interval → dp[i][j] = substring s[i..j]
  │  Palindrome, matrix chain, burst balloons
  ├─ Index + capacity → dp[i][w] = first i items, weight w
  │  Knapsack, subset sum
  └─ Index + state → dp[i][s] = at position i in state s
     Stock problems, paint house

SHORTCUT: if problem has two input arrays/strings → 2D
          if problem says "capacity" or "limit" → 2D
156 0/1 Knapsack template 15s

The inner loop goes BACKWARDS to prevent reusing item i. Forward loop = unbounded knapsack.

def knapsack(weights, values, W):
    n = len(weights)
    dp = [0] * (W + 1)
    for i in range(n):
        for w in range(W, weights[i] - 1, -1):  # backwards!
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
    return dp[W]
157 Longest Increasing Subsequence O(n²) 15s

dp[i] = length of longest increasing subsequence ending at index i. Classic O(n²) DP.

def lengthOfLIS(nums):
    n = len(nums)
    dp = [1] * n  # each element is a subsequence of length 1
    for i in range(1, n):
        for j in range(i):
            if nums[j] &lt; nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp)
158 LIS with binary search O(n log n) 15s

Patience sorting: maintain smallest possible tails. bisect_left finds where to place each element.

import bisect

def lengthOfLIS(nums):
    tails = []  # tails[i] = smallest tail of IS of length i+1
    for x in nums:
        pos = bisect.bisect_left(tails, x)
        if pos == len(tails):
            tails.append(x)
        else:
            tails[pos] = x
    return len(tails)
159 Longest Palindromic Substring 15s

Expand around center: try each index as center for odd and even palindromes. O(n²) time, O(1) space.

def longestPalindrome(s):
    res = ""
    for i in range(len(s)):
        # odd length
        l, r = i, i
        while l &gt;= 0 and r &lt; len(s) and s[l] == s[r]:
            if r - l + 1 &gt; len(res):
                res = s[l:r+1]
            l -= 1; r += 1
        # even length
        l, r = i, i + 1
        while l &gt;= 0 and r &lt; len(s) and s[l] == s[r]:
            if r - l + 1 &gt; len(res):
                res = s[l:r+1]
            l -= 1; r += 1
    return res
160 Longest Palindromic Subsequence 15s

2D interval DP. Iterate i from bottom-up so dp[i+1] is already computed. Equivalent to LCS(s, reverse(s)).

def longestPalindromeSubseq(s):
    n = len(s)
    dp = [[0] * n for _ in range(n)]
    for i in range(n - 1, -1, -1):
        dp[i][i] = 1
        for j in range(i + 1, n):
            if s[i] == s[j]:
                dp[i][j] = dp[i+1][j-1] + 2
            else:
                dp[i][j] = max(dp[i+1][j], dp[i][j-1])
    return dp[0][n-1]
161 Word Break 15s

dp[i] = can s[:i] be segmented? Try every split point j where dp[j] is True and s[j:i] is a word.

def wordBreak(s, wordDict):
    words = set(wordDict)
    n = len(s)
    dp = [False] * (n + 1)
    dp[0] = True
    for i in range(1, n + 1):
        for j in range(i):
            if dp[j] and s[j:i] in words:
                dp[i] = True
                break
    return dp[n]
162 Decode Ways 15s

Like climbing stairs but with validity checks. Single digit (1-9) adds dp[i-1], two digits (10-26) adds dp[i-2].

def numDecodings(s):
    if not s or s[0] == '0':
        return 0
    n = len(s)
    dp = [0] * (n + 1)
    dp[0] = dp[1] = 1
    for i in range(2, n + 1):
        if s[i-1] != '0':
            dp[i] += dp[i-1]
        two = int(s[i-2:i])
        if 10 &lt;= two &lt;= 26:
            dp[i] += dp[i-2]
    return dp[n]
163 House Robber 10s

At each house: rob it (prev2 + current) or skip it (prev1). Classic two-variable DP.

def rob(nums):
    if not nums: return 0
    if len(nums) == 1: return nums[0]
    prev2, prev1 = 0, 0
    for x in nums:
        prev2, prev1 = prev1, max(prev1, prev2 + x)
    return prev1
164 House Robber II (circular) 15s

Circular constraint: first and last are adjacent. Solve twice: once excluding first, once excluding last.

def rob(nums):
    if len(nums) == 1: return nums[0]
    def rob_linear(arr):
        prev2, prev1 = 0, 0
        for x in arr:
            prev2, prev1 = prev1, max(prev1, prev2 + x)
        return prev1
    return max(rob_linear(nums[1:]),   # skip first
               rob_linear(nums[:-1]))  # skip last
165 Min Cost Climbing Stairs 10s

dp[i] = cost[i] + min(dp[i-1], dp[i-2]). Optimized to two variables. Final answer is min of last two.

def minCostClimbingStairs(cost):
    a, b = cost[0], cost[1]
    for i in range(2, len(cost)):
        a, b = b, cost[i] + min(a, b)
    return min(a, b)
166 Buy/Sell Stock with Cooldown 15s

Three-state machine: hold → sell → rest → buy. After selling, must rest one day before buying.

def maxProfit(prices):
    hold = -prices[0]  # holding stock
    sold = 0           # just sold (cooldown next)
    rest = 0           # not holding, free to buy
    for p in prices[1:]:
        hold, sold, rest = (
            max(hold, rest - p),   # keep or buy
            hold + p,              # sell
            max(rest, sold),       # stay or come off cooldown
        )
    return max(sold, rest)
167 Buy/Sell Stock with Fee 10s

Two-state DP: cash (free to buy) and hold (free to sell). Fee paid on sell. No cooldown needed.

def maxProfit(prices, fee):
    cash = 0            # not holding
    hold = -prices[0]   # holding
    for p in prices[1:]:
        cash = max(cash, hold + p - fee)
        hold = max(hold, cash - p)
    return cash
168 Maximum Product Subarray 10s

Track both max and min product (negative * negative = positive). Swap on negative number.

def maxProduct(nums):
    res = mx = mn = nums[0]
    for x in nums[1:]:
        if x &lt; 0:
            mx, mn = mn, mx  # swap before multiply
        mx = max(x, mx * x)
        mn = min(x, mn * x)
        res = max(res, mx)
    return res
169 Kadane's Algorithm 10s

At each element: extend current subarray or start fresh. The simplest DP pattern — must be instant.

def maxSubArray(nums):
    cur = res = nums[0]
    for x in nums[1:]:
        cur = max(x, cur + x)
        res = max(res, cur)
    return res
170 Minimum Path Sum 15s

Classic 2D grid DP. Each cell = its value + min(top, left). In-place to avoid extra space.

def minPathSum(grid):
    m, n = len(grid), len(grid[0])
    for i in range(m):
        for j in range(n):
            if i == 0 and j == 0: continue
            elif i == 0: grid[i][j] += grid[i][j-1]
            elif j == 0: grid[i][j] += grid[i-1][j]
            else: grid[i][j] += min(grid[i-1][j], grid[i][j-1])
    return grid[m-1][n-1]
171 Triangle min path 15s

Bottom-up on triangle: each cell picks the smaller child. 1D array suffices since we overwrite left to right.

def minimumTotal(triangle):
    dp = triangle[-1][:]  # start from bottom row
    for i in range(len(triangle) - 2, -1, -1):
        for j in range(len(triangle[i])):
            dp[j] = triangle[i][j] + min(dp[j], dp[j+1])
    return dp[0]
172 Partition Equal Subset Sum 15s

Reduces to 0/1 knapsack: can we pick a subset summing to total/2? Backwards loop prevents item reuse.

def canPartition(nums):
    total = sum(nums)
    if total % 2: return False
    target = total // 2
    dp = [False] * (target + 1)
    dp[0] = True
    for x in nums:
        for j in range(target, x - 1, -1):  # backwards!
            dp[j] = dp[j] or dp[j - x]
    return dp[target]
173 Palindrome Partitioning DP precompute 15s

Two-phase DP: first build O(n²) palindrome lookup, then find min cuts. Common two-table pattern.

def minCut(s):
    n = len(s)
    # precompute palindrome table
    is_pal = [[False]*n for _ in range(n)]
    for i in range(n-1, -1, -1):
        for j in range(i, n):
            if s[i] == s[j] and (j - i &lt; 2 or is_pal[i+1][j-1]):
                is_pal[i][j] = True
    # dp[i] = min cuts for s[0..i]
    dp = list(range(n))  # worst case: cut before every char
    for i in range(1, n):
        if is_pal[0][i]:
            dp[i] = 0; continue
        for j in range(1, i+1):
            if is_pal[j][i]:
                dp[i] = min(dp[i], dp[j-1] + 1)
    return dp[n-1]
174 Burst Balloons (interval DP) 15s

Interval DP: dp[l][r] = max coins from bursting all balloons between l and r. k is the LAST one burst.

def maxCoins(nums):
    nums = [1] + nums + [1]
    n = len(nums)
    dp = [[0]*n for _ in range(n)]
    for length in range(2, n):  # gap between l and r
        for l in range(0, n - length):
            r = l + length
            for k in range(l+1, r):  # last balloon to burst
                dp[l][r] = max(dp[l][r],
                    dp[l][k] + dp[k][r] + nums[l]*nums[k]*nums[r])
    return dp[0][n-1]
175 Matrix Chain Multiplication pattern 15s

Interval DP template: enumerate gap size → left endpoint → split point. O(n³). Same structure as burst balloons.

def matrixChainOrder(dims):
    # dims[i-1] x dims[i] = matrix i dimensions
    n = len(dims) - 1
    dp = [[0]*n for _ in range(n)]
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            dp[i][j] = float('inf')
            for k in range(i, j):
                cost = (dp[i][k] + dp[k+1][j]
                        + dims[i]*dims[k+1]*dims[j+1])
                dp[i][j] = min(dp[i][j], cost)
    return dp[0][n-1]
176 Bitmask DP template 15s

Bitmask DP: represent subset as integer, each bit = included/excluded. O(2^n * n). For n ≤ 20.

def bitmaskDP(n, cost):
    # dp[mask] = best answer using subset 'mask'
    dp = [float('inf')] * (1 &lt;&lt; n)
    dp[0] = 0
    for mask in range(1 &lt;&lt; n):
        if dp[mask] == float('inf'): continue
        for i in range(n):
            if mask &amp; (1 &lt;&lt; i): continue  # already used
            new_mask = mask | (1 &lt;&lt; i)
            bits = bin(mask).count('1')  # position = items used so far
            dp[new_mask] = min(dp[new_mask],
                               dp[mask] + cost[bits][i])
    return dp[(1 &lt;&lt; n) - 1]
177 DP on digits template 15s

Digit DP: count numbers up to N with some property. 'tight' tracks if we're still bounded by N's digits.

from functools import lru_cache

def countDigitDP(num):
    digits = list(map(int, str(num)))
    @lru_cache(maxsize=None)
    def dp(pos, tight, started):
        if pos == len(digits):
            return 1 if started else 0
        limit = digits[pos] if tight else 9
        res = 0
        for d in range(0, limit + 1):
            res += dp(pos + 1,
                      tight and d == limit,
                      started or d &gt; 0)
        return res
    return dp(0, True, False)
178 Regular Expression Matching 15s

2D DP matching string vs pattern. '*' means zero or more of preceding element. '.' matches any single char.

def isMatch(s, p):
    m, n = len(s), len(p)
    dp = [[False]*(n+1) for _ in range(m+1)]
    dp[0][0] = True
    for j in range(1, n+1):
        if p[j-1] == '*':
            dp[0][j] = dp[0][j-2]  # x* matches empty
    for i in range(1, m+1):
        for j in range(1, n+1):
            if p[j-1] == '*':
                dp[i][j] = dp[i][j-2]  # zero occurrences
                if p[j-2] == '.' or p[j-2] == s[i-1]:
                    dp[i][j] |= dp[i-1][j]  # one+ occurrences
            elif p[j-1] == '.' or p[j-1] == s[i-1]:
                dp[i][j] = dp[i-1][j-1]
    return dp[m][n]
179 Wildcard Matching 15s

'*' matches any sequence (dp[i-1][j] = consume char, dp[i][j-1] = empty match). '?' matches any single char.

def isMatch(s, p):
    m, n = len(s), len(p)
    dp = [[False]*(n+1) for _ in range(m+1)]
    dp[0][0] = True
    for j in range(1, n+1):
        if p[j-1] == '*':
            dp[0][j] = dp[0][j-1]
    for i in range(1, m+1):
        for j in range(1, n+1):
            if p[j-1] == '*':
                dp[i][j] = dp[i-1][j] or dp[i][j-1]
            elif p[j-1] == '?' or p[j-1] == s[i-1]:
                dp[i][j] = dp[i-1][j-1]
    return dp[m][n]
180 Maximal Square 15s

dp[i][j] = side length of largest square with bottom-right at (i,j). min of three neighbors + 1.

def maximalSquare(matrix):
    m, n = len(matrix), len(matrix[0])
    dp = [[0]*n for _ in range(m)]
    res = 0
    for i in range(m):
        for j in range(n):
            if matrix[i][j] == '1':
                if i == 0 or j == 0:
                    dp[i][j] = 1
                else:
                    dp[i][j] = min(dp[i-1][j], dp[i][j-1],
                                   dp[i-1][j-1]) + 1
                res = max(res, dp[i][j])
    return res * res
239 Recursion vs iteration: how to convert 15s

Every recursion can be converted to iteration with an explicit stack. Tail recursion converts to a simple loop.

RECURSION → ITERATION CONVERSION
─────────────────────────────────
STEP 1: Identify what the call stack stores
  → Parameters + local variables + return point

STEP 2: Replace call stack with explicit stack
  stack = [(initial_params)]
  while stack:
      params = stack.pop()
      # ... process ...
      stack.append(next_params)  # instead of recursive call

STEP 3: Handle return values
  → Use a result variable or secondary stack

TAIL RECURSION (easy):
  # Recursive:
  def f(n, acc=0):
      if n == 0: return acc
      return f(n-1, acc+n)
  # Iterative:
  acc = 0
  while n &gt; 0: acc += n; n -= 1

TREE RECURSION (use stack):
  # Recursive:          # Iterative:
  def dfs(node):         stack = [root]
      process(node)      while stack:
      dfs(node.left)         node = stack.pop()
      dfs(node.right)        process(node)
                             if node.right: stack.append(node.right)
                             if node.left: stack.append(node.left)
240 Greedy vs DP: key difference 15s

Greedy never backtracks. DP explores all options. If you can't prove greedy works with an exchange argument, use DP.

GREEDY vs DP — KEY DIFFERENCE
─────────────────────────────
GREEDY: make locally optimal choice at each step
  → Never reconsider previous choices
  → No overlapping subproblems needed
  → O(n) or O(n log n) typically

DP: consider ALL choices, pick globally optimal
  → May revisit states through memoization
  → Requires overlapping subproblems
  → O(n²) or O(n*W) typically

HOW TO TELL:
  □ Can you prove greedy works? (exchange argument)
  □ Does local best = global best? → greedy
  □ Counterexample found? → need DP

CLASSIC GREEDY:
  Activity selection, interval scheduling, Huffman,
  jump game, gas station, assign cookies

CLASSIC DP:
  Knapsack, coin change, LCS, edit distance

TRAP: coin change is DP (greedy fails for [1,3,4] target=6)
      but interval scheduling is greedy (sort by end time)