Recursion and Memoization Interview Patterns: LCS, Edit Distance, Word Break

Recursion and Memoization Interview Patterns

Memoization transforms exponential recursive solutions into polynomial ones by caching subproblem results. In Python, @lru_cache makes memoization trivial. This guide covers the patterns for recognizing when memoization applies and the most important problems.

When Memoization Applies

Memoization works when a problem has:

  1. Overlapping subproblems: the same subproblem is computed multiple times in a naive recursion
  2. Optimal substructure: the optimal solution to a problem can be constructed from optimal solutions to subproblems

Signs you should memoize: the recursive tree has exponential branching (2^n calls) but only polynomial distinct states (n^2 distinct (i, j) pairs).

Python Memoization with lru_cache

from functools import lru_cache

@lru_cache(maxsize=None)   # unlimited cache
def fib(n):
    if n <= 1: return n
    return fib(n-1) + fib(n-2)
# Without cache: O(2^n) calls. With cache: O(n) unique calls.

For problems with mutable arguments (lists, dicts), convert to tuples before passing to cached functions.

Coin Change (LeetCode 322)

def coin_change(coins, amount):
    @lru_cache(maxsize=None)
    def dp(remaining):
        if remaining < 0: return float('inf')
        if remaining == 0: return 0
        return 1 + min(dp(remaining - c) for c in coins)
    result = dp(amount)
    return result if result != float('inf') else -1
# State: remaining amount (0 to amount)
# Transitions: try each coin denomination
# Time: O(amount * len(coins)), Space: O(amount)

Longest Common Subsequence (LeetCode 1143)

def lcs(s1, s2):
    @lru_cache(maxsize=None)
    def dp(i, j):
        if i == len(s1) or j == len(s2): return 0
        if s1[i] == s2[j]:
            return 1 + dp(i+1, j+1)
        return max(dp(i+1, j), dp(i, j+1))
    return dp(0, 0)
# State: (i, j) = position in s1 and s2
# O(m*n) states, O(1) per state = O(m*n) total

Word Break (LeetCode 139)

def word_break(s, wordDict):
    words = set(wordDict)
    @lru_cache(maxsize=None)
    def dp(start):
        if start == len(s): return True
        for end in range(start + 1, len(s) + 1):
            if s[start:end] in words and dp(end):
                return True
        return False
    return dp(0)
# State: start index (0 to n)
# Try all prefixes from start — O(n^2) states * O(n) string check = O(n^3)

Regular Expression Matching (LeetCode 10)

def is_match(s, p):
    @lru_cache(maxsize=None)
    def dp(i, j):
        if j == len(p): return i == len(s)
        first_match = i < len(s) and (p[j] == s[i] or p[j] == '.')
        if j + 1 < len(p) and p[j+1] == '*':
            # either skip "x*" entirely, or use it (if first char matches)
            return dp(i, j+2) or (first_match and dp(i+1, j))
        return first_match and dp(i+1, j+1)
    return dp(0, 0)
# State: (i, j) = position in s and p
# O(m*n) states

Edit Distance (LeetCode 72)

def min_distance(word1, word2):
    @lru_cache(maxsize=None)
    def dp(i, j):
        if i == 0: return j   # insert all remaining chars of word2
        if j == 0: return i   # delete all remaining chars of word1
        if word1[i-1] == word2[j-1]:
            return dp(i-1, j-1)   # no operation needed
        return 1 + min(
            dp(i-1, j),     # delete from word1
            dp(i, j-1),     # insert into word1
            dp(i-1, j-1)    # replace
        )
    return dp(len(word1), len(word2))
# Classic interview problem: Levenshtein distance
# State: (i, j) = first i chars of word1, first j chars of word2

Decode Ways (LeetCode 91)

def num_decodings(s):
    @lru_cache(maxsize=None)
    def dp(i):
        if i == len(s): return 1   # decoded successfully
        if s[i] == '0': return 0   # leading zero — invalid
        result = dp(i + 1)         # single digit
        if i + 1 < len(s) and int(s[i:i+2]) <= 26:
            result += dp(i + 2)    # two digits
        return result
    return dp(0)

Memoization vs. Bottom-Up DP

Aspect Memoization (top-down) Bottom-up DP
Order Automatic (lazy) Manual (must figure out)
Only needed states Yes (sparse problems) No (fills all states)
Stack overflow risk Yes (deep recursion) No
Space optimization Hard (cache stores all) Easy (rolling array)
Interview speed Faster to write Requires careful indexing

Interview Tips

  • Use @lru_cache in Python — it is production-grade and acceptable in interviews
  • Always state the number of distinct states and time per state to derive total complexity
  • For 2D memoization (LCS, edit distance, regex matching), the state is (i, j) — a 2D table
  • Convert mutable parameters to tuples for lru_cache: dp(tuple(coins), amount)
  • When interviewers ask “can you do it bottom-up?” — convert by filling the dp array in reverse order of the recursion

  • Uber Interview Guide
  • Snap Interview Guide
  • Atlassian Interview Guide
  • LinkedIn Interview Guide
  • Apple Interview Guide
  • Meta Interview Guide
  • {
    “@context”: “https://schema.org”,
    “@type”: “FAQPage”,
    “mainEntity”: [
    {
    “@type”: “Question”,
    “name”: “What is the difference between memoization and tabulation in dynamic programming?”,
    “acceptedAnswer”: { “@type”: “Answer”, “text”: “Memoization (top-down): implement the recursive solution naturally, add caching (lru_cache or a dict) to avoid recomputing subproblems. The recursion naturally computes only the needed subproblems. Easy to write, risk of stack overflow for deep recursion. Tabulation (bottom-up): fill a DP table iteratively from smallest subproblems to largest. No recursion overhead, easy to optimize space with rolling arrays, but you must manually determine the correct fill order. When to use each: use memoization for irregular recursion trees (word break, regular expression matching) where many subproblems may not be needed. Use tabulation when you need space optimization (coin change with O(amount) space instead of O(amount * coins)), or when recursion depth exceeds Python's default limit (~1000 levels). In interviews, memoization with @lru_cache is faster to write and equally valid.” }
    },
    {
    “@type”: “Question”,
    “name”: “How do you approach edit distance (Levenshtein) in a coding interview?”,
    “acceptedAnswer”: { “@type”: “Answer”, “text”: “Edit distance is the minimum number of insertions, deletions, or replacements to convert word1 to word2. State: dp(i, j) = min edits to convert word1[:i] to word2[:j]. Base cases: dp(i, 0) = i (delete all of word1), dp(0, j) = j (insert all of word2). Transitions: if word1[i-1] == word2[j-1], no edit needed: dp(i,j) = dp(i-1, j-1). Otherwise, take the minimum of: dp(i-1, j) + 1 (delete from word1), dp(i, j-1) + 1 (insert into word1), dp(i-1, j-1) + 1 (replace). Time O(m*n), space O(m*n) or O(min(m,n)) with rolling array. Variants: if you only allow insertions and deletions (no replacements), edit distance = m + n – 2 * LCS(word1, word2). Mention this connection to LCS – it impresses interviewers.” }
    },
    {
    “@type”: “Question”,
    “name”: “How do you solve Word Break with memoization and what is the time complexity?”,
    “acceptedAnswer”: { “@type”: “Answer”, “text”: “Word Break: given string s and dictionary of words, can s be segmented into dictionary words? Top-down: dp(start) = can s[start:] be segmented? Base case: dp(len(s)) = True (empty remainder). For each start, try all ends from start+1 to len(s)+1: if s[start:end] is in the word set AND dp(end) is True, then dp(start) = True. Memoize dp with @lru_cache. Time: O(n^2) states (n start positions) * O(n) per state (trying n split points, each requiring O(n) string slice check) = O(n^3). With a Trie instead of string slicing: O(n^2). State space: O(n) unique start positions. This is a good example where memoization outperforms backtracking: without memoization, the naive backtracking re-explores the same suffix many times.” }
    }
    ]
    }

    Scroll to Top