Interleaving String: 2D DP on Two Strings
You are given three strings s1, s2, and s3. Determine whether s3 is formed by an interleaving of s1 and s2. An interleaving means you weave the characters of s1 and s2 together, choosing the next character from either string at each step, while preserving the left-to-right order within each source string.
This is LeetCode 97: Interleaving String, and it is a clean example of 2D boolean DP on two strings. If you have worked through Edit Distance or Longest Common Subsequence, you already know the grid structure. The difference here is that each cell stores True or False instead of a numeric cost, and the transitions come from two directions (above and left) instead of three.
Why this problem matters
Interleaving String is a great problem for building fluency with 2D DP on two strings. Unlike Edit Distance, where you minimize a cost, or LCS, where you maximize a length, this problem asks a yes/no reachability question. The cell dp[i][j] answers: "Can I form the first i + j characters of s3 using exactly the first i characters of s1 and the first j characters of s2?"
That boolean framing changes how you think about transitions. You are not picking a minimum or maximum. You are checking whether at least one valid predecessor exists. This is a useful mental shift, because many DP problems in interviews use boolean reachability rather than optimization.
The brute force idea
Try every possible way to split s3 into characters from s1 and s2. At position k in s3, if s3[k] matches the current character in s1, try taking it from s1. If it matches the current character in s2, try taking it from s2. If neither matches, backtrack.
def is_interleave_recursive(s1, s2, s3, i, j, k):
if k == len(s3):
return i == len(s1) and j == len(s2)
result = False
if i < len(s1) and s1[i] == s3[k]:
result = is_interleave_recursive(s1, s2, s3, i + 1, j, k + 1)
if not result and j < len(s2) and s2[j] == s3[k]:
result = is_interleave_recursive(s1, s2, s3, i, j + 1, k + 1)
return result
This is exponential because the same (i, j) pair (note that k = i + j always) gets revisited many times. You need memoization, or better, a bottom-up table.
The brute force has O(2^(m+n)) worst-case time. Even with memoization, the bottom-up approach is cleaner and avoids recursion limits. Build the table directly.
Building the 2D DP table
Define dp[i][j] as True if s1[:i] and s2[:j] can interleave to form s3[:i+j]. The table has (m + 1) rows and (n + 1) columns, where m = len(s1) and n = len(s2).
The recurrence checks two predecessors:
- From above (
dp[i-1][j]): ifdp[i-1][j]is True ands1[i-1] == s3[i+j-1], then taking the next character froms1extends a valid interleaving. Setdp[i][j] = True. - From the left (
dp[i][j-1]): ifdp[i][j-1]is True ands2[j-1] == s3[i+j-1], then taking the next character froms2extends a valid interleaving. Setdp[i][j] = True.
If neither condition holds, dp[i][j] = False.
Base cases:
dp[0][0] = True. Two empty strings interleave to form an empty string.- First row
dp[0][j]: onlys2contributes.dp[0][j] = dp[0][j-1] and s2[j-1] == s3[j-1]. - First column
dp[i][0]: onlys1contributes.dp[i][0] = dp[i-1][0] and s1[i-1] == s3[i-1].
Before filling the table, check the length constraint: if len(s1) + len(s2) != len(s3), return False immediately. The lengths must add up.
Let's trace through s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac":
Initialize: base cases along the first row and first column
dp[0][0] = True (empty + empty = empty). First column: s3[0]="a" matches s1[0]="a", so dp[1][0]=True. s3[1]="a" matches s1[1]="a", but we have not filled row 2 yet. First row: s3[0]="a" does not match s2[0]="d", so dp[0][1]=False, and all remaining first-row cells are False.
Fill base cases fully: first column continues, first row stays False
dp[2][0]: s3[1]="a" matches s1[1]="a" and dp[1][0]=True, so True. dp[3][0]: s3[2]="d" does not match s1[2]="b", so False. The chain breaks, and dp[4][0] and dp[5][0] are also False.
Row 2 (s1="aa"): the interleaving path opens up through s2
dp[2][1]: s3[2]="d" matches s2[0]="d" and dp[2][0]=True. True. The rest of row 2 chains through s2: s3[3]="b"==s2[1]="b", s3[4]="b"==s2[2]="b", s3[5]="c"==s2[3]="c". But s3[6]="b" does not match s2[4]="a", so dp[2][5]=False.
Row 3 (s1="aab"): taking from s1 or s2 at each cell
dp[3][1]: s3[3]="b" matches s1[2]="b" and dp[2][1]=True. True (taking from s1). dp[3][2]: s3[4]="b" matches s2[1]="b" and dp[3][1]=True. True (taking from s2). dp[3][3]: neither s1[2]="b" nor s2[2]="b" matches s3[5]="c" with a True neighbor. False.
Rows 4-5: the path converges to dp[5][5] = True
dp[4][2]: s3[5]="c" matches s1[3]="c" and dp[3][2]=True. dp[4][5]: s3[8]="a" matches s2[4]="a" and dp[4][4]=True. dp[5][5]: s3[9]="c" matches s1[4]="c" and dp[4][5]=True. Answer: True.
The answer is dp[5][5] = True. You can form s3 by interleaving s1 and s2.
def is_interleave(s1, s2, s3):
m, n = len(s1), len(s2)
if m + n != len(s3):
return False
dp = [[False] * (n + 1) for _ in range(m + 1)]
dp[0][0] = True
for i in range(1, m + 1):
dp[i][0] = dp[i - 1][0] and s1[i - 1] == s3[i - 1]
for j in range(1, n + 1):
dp[0][j] = dp[0][j - 1] and s2[j - 1] == s3[j - 1]
for i in range(1, m + 1):
for j in range(1, n + 1):
dp[i][j] = (
(dp[i - 1][j] and s1[i - 1] == s3[i + j - 1])
or (dp[i][j - 1] and s2[j - 1] == s3[i + j - 1])
)
return dp[m][n]
Time: O(m * n). Every cell in the 2D table gets filled exactly once.
Space: O(m * n). The full table.
The key insight is that k = i + j at every cell. You never need a third index. The position in s3 is always determined by how many characters you have consumed from s1 and s2 combined. This is what makes the 2D table sufficient.
Understanding the two transitions visually
Each cell dp[i][j] depends on at most two neighbors:
- Above
dp[i-1][j]: you took the next character froms1. This meanss1[i-1]must matchs3[i+j-1]. Moving down a row in the table means consuming one character froms1. - Left
dp[i][j-1]: you took the next character froms2. This meanss2[j-1]must matchs3[i+j-1]. Moving right a column means consuming one character froms2.
There is no diagonal transition here. In Edit Distance and LCS, the diagonal represents "both strings advance." In interleaving, you take from exactly one string at each step, so the cell depends only on the cell above or the cell to the left.
Think of it as a path through the grid from (0, 0) to (m, n). Each step goes either down (take from s1) or right (take from s2). A valid interleaving corresponds to a path where every step matches the next character of s3.
Optimizing space to O(n)
Each row of the table only depends on the current row (left neighbor) and the row above it (above neighbor). You can drop the 2D grid down to a single 1D array and update it in place.
def is_interleave(s1, s2, s3):
m, n = len(s1), len(s2)
if m + n != len(s3):
return False
dp = [False] * (n + 1)
dp[0] = True
for j in range(1, n + 1):
dp[j] = dp[j - 1] and s2[j - 1] == s3[j - 1]
for i in range(1, m + 1):
dp[0] = dp[0] and s1[i - 1] == s3[i - 1]
for j in range(1, n + 1):
dp[j] = (
(dp[j] and s1[i - 1] == s3[i + j - 1])
or (dp[j - 1] and s2[j - 1] == s3[i + j - 1])
)
return dp[n]
Time: O(m * n). Same.
Space: O(n). One row of length n + 1.
In the space-optimized version, dp[j] before the update holds the value from the previous row (the "above" neighbor), and dp[j-1] holds the already-updated value from the current row (the "left" neighbor). This works because you iterate left to right, so by the time you need dp[j-1], it has already been updated for the current row.
Complexity analysis
| Approach | Time | Space |
|---|---|---|
| Brute force recursion | O(2^(m+n)) | O(m + n) call stack |
| 2D DP table | O(m * n) | O(m * n) |
| Space-optimized (one row) | O(m * n) | O(n) |
Where m = length of s1, n = length of s2.
Edge cases to watch for
- Length mismatch: if
len(s1) + len(s2) != len(s3), return False immediately. This is the most important early check. - Both strings empty:
s1 = "",s2 = "",s3 = "". Return True. The base casedp[0][0] = Truehandles this. - One string empty: if
s1is empty, check whethers2 == s3. Ifs2is empty, check whethers1 == s3. The first row or first column of the table handles this naturally. - No valid interleaving:
s1 = "aabcc",s2 = "dbbca",s3 = "aadbbbaccc". The characters are all present, but the ordering does not work.dp[m][n]will be False. - Identical characters in both strings: e.g.,
s1 = "aa",s2 = "aa",s3 = "aaaa". Multiple valid interleavings exist, but you only need to find one. The DP table will have many True cells.
The building blocks
This problem is built on 2D string DP and character matching from two sources. The structure is:
- State:
dp[i][j]= cans1[:i]ands2[:j]forms3[:i+j]? - Transition: check above (take from s1) or left (take from s2), each gated by a character match against
s3[i+j-1] - Base case:
dp[0][0] = True, first row checks s2 against s3, first column checks s1 against s3
The 2D string DP building block is the same one that powers Edit Distance and Longest Common Subsequence. The table dimensions, the row-by-row fill order, and the space optimization technique are identical across these problems. What changes is the recurrence: LCS maximizes, Edit Distance minimizes, and Interleaving String checks boolean reachability.
The character matching from two sources building block is specific to this problem. At each step, you decide which of two input strings provides the next character. This two-source matching idea also appears in problems like merging sorted arrays and certain game-theory DP problems.
From understanding to recall
You now know how to set up the boolean 2D table, why each cell depends on its above and left neighbors, and how the space optimization works. But understanding the pattern once is not the same as producing it under pressure in an interview.
The tricky parts are: remembering that k = i + j eliminates the need for a third dimension, getting the base cases right for the first row and first column, and writing the boolean OR transition without confusing it with the min/max transitions from Edit Distance or LCS. Those details are exactly the kind of thing that slips away after a week.
The 2D string DP building block is one of roughly 60 reusable patterns in the CodeBricks system. You drill each block individually with spaced repetition until writing the solution from scratch is automatic. Interleaving String, Edit Distance, and LCS together form the core of the two-string DP family. Locking all three in early means you can handle any variant that shows up in an interview.
Related posts
- Edit Distance - The classic 2D string DP problem that minimizes edit cost instead of checking boolean reachability
- Longest Common Subsequence - The foundational two-string DP problem that maximizes shared subsequence length