String DP Interview Patterns: LCS, Edit Distance, Palindrome DP

String dynamic programming problems — longest common subsequence, edit distance, palindromic subsequences — appear frequently in senior engineering interviews. These problems share a 2D DP table structure where the state represents two substring positions.

Template: 2D String DP

Most string DP problems follow this template:
  dp[i][j] = answer for s1[0..i-1] and s2[0..j-1] (first i chars of s1, j chars of s2)

Base cases: dp[0][j] = 0 (empty s1), dp[i][0] = 0 (empty s2)

Recurrence depends on whether s1[i-1] == s2[j-1]:
  Case 1 (match):   dp[i][j] = dp[i-1][j-1] + (some value)
  Case 2 (no match): dp[i][j] = some combination of dp[i-1][j], dp[i][j-1], dp[i-1][j-1]

Pattern 1: Longest Common Subsequence (LCS)

def lcs(s1: str, s2: str) -> int:
    """LC 1143: length of longest common subsequence"""
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i-1] == s2[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1  # extend LCS
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])  # skip one char
    return dp[m][n]  # Time O(mn), Space O(mn) → O(n) with rolling array

# Reconstruct the actual LCS:
def lcs_string(s1, s2):
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1
            else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    # Backtrack to recover sequence
    result = []
    i, j = m, n
    while i > 0 and j > 0:
        if s1[i-1] == s2[j-1]:
            result.append(s1[i-1]); i -= 1; j -= 1
        elif dp[i-1][j] > dp[i][j-1]:
            i -= 1
        else:
            j -= 1
    return ''.join(reversed(result))

Pattern 2: Edit Distance (Levenshtein)

def minDistance(word1: str, word2: str) -> int:
    """LC 72: minimum operations (insert, delete, replace) to convert word1 → word2"""
    m, n = len(word1), len(word2)
    # dp[i][j] = edit distance between word1[:i] and word2[:j]
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    # Base cases: converting to/from empty string
    for i in range(m + 1): dp[i][0] = i  # delete all chars
    for j in range(n + 1): dp[0][j] = j  # insert all chars
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if word1[i-1] == word2[j-1]:
                dp[i][j] = dp[i-1][j-1]  # no operation needed
            else:
                dp[i][j] = 1 + min(
                    dp[i-1][j],    # delete from word1
                    dp[i][j-1],    # insert into word1
                    dp[i-1][j-1]   # replace
                )
    return dp[m][n]

# Space optimization to O(n):
def minDistance_optimized(word1, word2):
    m, n = len(word1), len(word2)
    prev = list(range(n + 1))
    for i in range(1, m + 1):
        curr = [i] + [0] * n
        for j in range(1, n + 1):
            if word1[i-1] == word2[j-1]:
                curr[j] = prev[j-1]
            else:
                curr[j] = 1 + min(prev[j], curr[j-1], prev[j-1])
        prev = curr
    return prev[n]

Pattern 3: Longest Palindromic Subsequence

def longestPalindromeSubseq(s: str) -> int:
    """LC 516: O(n²) — LPS = LCS(s, reverse(s))"""
    return lcs(s, s[::-1])

# Direct DP solution:
def lps_direct(s: str) -> int:
    n = len(s)
    # dp[i][j] = LPS length in s[i..j]
    dp = [[0] * n for _ in range(n)]
    for i in range(n): dp[i][i] = 1  # single char is palindrome
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            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]

Pattern 4: Longest Palindromic Substring

def longestPalindrome(s: str) -> str:
    """LC 5: O(n²) — expand around center"""
    best_start = best_len = 0
    def expand(left, right):
        nonlocal best_start, best_len
        while left >= 0 and right  best_len:
                best_start = left
                best_len = right - left + 1
            left -= 1; right += 1
    for i in range(len(s)):
        expand(i, i)      # odd length
        expand(i, i + 1)  # even length
    return s[best_start:best_start + best_len]

# DP approach O(n²) time and space:
def longestPalindrome_dp(s: str) -> str:
    n = len(s)
    dp = [[False] * n for _ in range(n)]
    start = length = 0
    for i in range(n): dp[i][i] = True
    for i in range(n - 1):
        if s[i] == s[i+1]:
            dp[i][i+1] = True; start, length = i, 2
    for l in range(3, n + 1):
        for i in range(n - l + 1):
            j = i + l - 1
            if s[i] == s[j] and dp[i+1][j-1]:
                dp[i][j] = True
                if l > length: start, length = i, l
    return s[start:start + length]

Pattern 5: Minimum Insertions/Deletions for Palindrome

def minInsertions(s: str) -> int:
    """LC 1312: minimum insertions to make string palindrome"""
    # Observation: LPS characters stay, rest need insertion for mirroring
    # min_insertions = n - LPS_length
    return len(s) - lps_direct(s)

def minOperationsToMakePalindrome(s: str) -> int:
    """Minimum deletions: n - LPS (delete non-palindromic chars)"""
    n = len(s)
    lps = lcs(s, s[::-1])
    return n - lps  # delete non-LPS characters

Pattern 6: Shortest Common Supersequence (LC 1092)

def shortestCommonSupersequence(s1: str, s2: str) -> str:
    """Shortest string containing both s1 and s2 as subsequences"""
    # Length: len(s1) + len(s2) - LCS(s1, s2)
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m+1):
        for j in range(1, n+1):
            if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1
            else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    # Reconstruct: include LCS chars once, other chars from their string
    result = []
    i, j = m, n
    while i > 0 and j > 0:
        if s1[i-1] == s2[j-1]:
            result.append(s1[i-1]); i -= 1; j -= 1
        elif dp[i-1][j] > dp[i][j-1]:
            result.append(s1[i-1]); i -= 1
        else:
            result.append(s2[j-1]); j -= 1
    result.extend(reversed(s1[:i]))
    result.extend(reversed(s2[:j]))
    return ''.join(reversed(result))

Pattern 7: Distinct Subsequences (LC 115)

def numDistinct(s: str, t: str) -> int:
    """Count distinct subsequences of s that equal t"""
    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 (take nothing)
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            dp[i][j] = dp[i-1][j]  # skip s[i-1]
            if s[i-1] == t[j-1]:
                dp[i][j] += dp[i-1][j-1]  # also use s[i-1] to match t[j-1]
    return dp[m][n]

String DP Problem Recognition

Problem Type DP State Key Recurrence
LCS / common subsequence dp[i][j] = length using s1[:i], s2[:j] match: +1; no match: max(skip either)
Edit distance dp[i][j] = min ops for s1[:i] → s2[:j] match: 0; no match: 1+min(del,ins,rep)
Palindrome (subsequence) dp[i][j] = LPS in s[i..j] match: dp[i+1][j-1]+2; no: max(skip)
Count ways dp[i][j] = number of ways match: +previous; no match: carry forward

Key Interview Tips

  • Space optimization: Most string DP problems use only the current and previous rows. Replace the 2D table with two 1D arrays (prev, curr) for O(n) space. For single-string palindrome DP, the table is symmetric — compute the lower triangle only.
  • LCS as a building block: Many string DP problems reduce to LCS. Palindrome = LCS(s, reverse(s)). Shortest common supersequence = len(s1)+len(s2)-LCS. Minimum deletions to make palindrome = len(s)-LCS(s, reverse(s)).
  • Expand-around-center vs DP for palindromes: Expand-around-center is O(n²) time and O(1) space (no DP table) — prefer it for longest palindromic substring. DP is O(n²) time and O(n²) space — use when you need palindrome information for all sub-ranges (e.g., palindrome partitioning).
  • Manacher’s algorithm: O(n) longest palindromic substring — rarely required in interviews but impressive to mention. The interviewable solution is expand-around-center.

  • Snap Interview Guide
  • Atlassian Interview Guide
  • LinkedIn Interview Guide
  • Uber Interview Guide
  • Apple Interview Guide
  • Companies That Ask This

    Asked at: Coinbase Interview Guide

    Scroll to Top