A detailed guide to solving Interleaving String with two-dimensional dynamic programming.
Problem Restatement
We are given three strings:
s1: str
s2: str
s3: strWe need to decide whether s3 can be formed by interleaving s1 and s2.
Interleaving means we take all characters from s1 and all characters from s2, while preserving the original order inside each string.
The characters may be mixed together.
For example:
s1 = "abc"
s2 = "def"A valid interleaving is:
"adbcef"because the characters from s1 still appear as:
a -> b -> cand the characters from s2 still appear as:
d -> e -> fThe official problem asks whether s3 is formed by an interleaving of s1 and s2. The examples include s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac" returning true, and s3 = "aadbbbaccc" returning false. The constraints allow s1.length and s2.length up to 100, and s3.length up to 200.
Input and Output
| Item | Meaning |
|---|---|
| Input | Strings s1, s2, and s3 |
| Output | True if s3 is an interleaving, otherwise False |
Order rule for s1 | Characters from s1 must appear in original order |
Order rule for s2 | Characters from s2 must appear in original order |
| Length rule | len(s1) + len(s2) must equal len(s3) |
Function shape:
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
...Examples
Example 1:
s1 = "aabcc"
s2 = "dbbca"
s3 = "aadbbcbcac"This is valid.
One way to read s3 is:
aa from s1
dbbc from s2
bc from s1
a from s2
c from s1The order inside s1 is preserved.
The order inside s2 is preserved.
Answer:
TrueExample 2:
s1 = "aabcc"
s2 = "dbbca"
s3 = "aadbbbaccc"This cannot be formed while preserving both source orders.
Answer:
FalseExample 3:
s1 = ""
s2 = ""
s3 = ""Both source strings are empty.
The target is also empty.
Answer:
TrueFirst Thought: Recursion
At any point while building s3, we may take the next character from s1 or the next character from s2.
Suppose we have already used:
i characters from s1
j characters from s2Then the next position in s3 is:
i + jThere are at most two choices:
- If
s1[i] == s3[i + j], take the next character froms1. - If
s2[j] == s3[i + j], take the next character froms2.
A direct recursion follows this idea, but it repeats the same states many times.
So we use dynamic programming.
Key Insight
Define:
dp[i][j]to mean:
s3[:i + j] can be formed by interleaving s1[:i] and s2[:j]So:
| State | Meaning |
|---|---|
i | Number of characters taken from s1 |
j | Number of characters taken from s2 |
i + j | Number of characters formed in s3 |
The answer is:
dp[len(s1)][len(s2)]Length Check
Before doing DP, check the total length.
if len(s1) + len(s2) != len(s3):
return FalseIf the lengths do not match, s3 cannot use exactly all characters from s1 and s2.
Transition
For state dp[i][j], the last character of s3[:i + j] came from either s1 or s2.
Case 1: Last Character Came From s1
This is possible if:
i > 0and:
s1[i - 1] == s3[i + j - 1]Then we need the previous state:
dp[i - 1][j]So:
dp[i][j] = dp[i][j] or dp[i - 1][j]Case 2: Last Character Came From s2
This is possible if:
j > 0and:
s2[j - 1] == s3[i + j - 1]Then we need the previous state:
dp[i][j - 1]So:
dp[i][j] = dp[i][j] or dp[i][j - 1]Algorithm
Let:
m = len(s1)
n = len(s2)Create:
dp = [[False] * (n + 1) for _ in range(m + 1)]Set:
dp[0][0] = TrueThen fill the table.
For every i from 0 to m, and every j from 0 to n:
- If the next state can come from
s1, usedp[i - 1][j]. - If the next state can come from
s2, usedp[i][j - 1].
Return:
dp[m][n]Walkthrough
Use:
s1 = "aabcc"
s2 = "dbbca"
s3 = "aadbbcbcac"Start:
dp[0][0] = TrueThe first character of s3 is "a".
It can only come from s1[0], because s2[0] is "d".
So:
dp[1][0] = TrueThe next character of s3 is also "a".
It can come from s1[1].
So:
dp[2][0] = TrueThe next character is "d".
It comes from s2[0].
So:
dp[2][1] = TrueThe DP continues this way, always checking whether the next character can be explained by taking from s1 or from s2.
At the end:
dp[5][5] = TrueSo the answer is:
TrueCorrectness
The DP state dp[i][j] represents whether the first i + j characters of s3 can be formed from the first i characters of s1 and the first j characters of s2.
The base case dp[0][0] = True is correct because two empty prefixes form an empty prefix.
For any non-empty state, the last character of s3[:i + j] must come from either the last used character of s1[:i] or the last used character of s2[:j].
If it comes from s1, then s1[i - 1] must equal s3[i + j - 1], and the previous characters must form a valid interleaving represented by dp[i - 1][j].
If it comes from s2, then s2[j - 1] must equal s3[i + j - 1], and the previous characters must form a valid interleaving represented by dp[i][j - 1].
The transition checks exactly these two cases.
Therefore, by induction over i + j, every DP entry is correct. The final entry dp[m][n] answers whether all of s1 and all of s2 form all of s3.
Complexity
Let:
m = len(s1)
n = len(s2)| Metric | Value | Why |
|---|---|---|
| Time | O(m * n) | There are (m + 1) * (n + 1) states |
| Space | O(m * n) | The DP table stores all states |
Implementation
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
m = len(s1)
n = 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(m + 1):
for j in range(n + 1):
if i > 0 and s1[i - 1] == s3[i + j - 1]:
dp[i][j] = dp[i][j] or dp[i - 1][j]
if j > 0 and s2[j - 1] == s3[i + j - 1]:
dp[i][j] = dp[i][j] or dp[i][j - 1]
return dp[m][n]Code Explanation
First store the string lengths:
m = len(s1)
n = len(s2)If the total length differs, return immediately:
if m + n != len(s3):
return FalseCreate the DP table:
dp = [[False] * (n + 1) for _ in range(m + 1)]The empty prefixes match:
dp[0][0] = TrueThen fill every state:
for i in range(m + 1):
for j in range(n + 1):If the last character came from s1:
if i > 0 and s1[i - 1] == s3[i + j - 1]:
dp[i][j] = dp[i][j] or dp[i - 1][j]If the last character came from s2:
if j > 0 and s2[j - 1] == s3[i + j - 1]:
dp[i][j] = dp[i][j] or dp[i][j - 1]Finally:
return dp[m][n]Space-Optimized Implementation
Since each row only depends on the previous row and the current row, we can use a one-dimensional DP array.
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
m = len(s1)
n = len(s2)
if m + n != len(s3):
return False
dp = [False] * (n + 1)
dp[0] = True
for i in range(m + 1):
for j in range(n + 1):
if i == 0 and j == 0:
continue
from_s1 = (
i > 0
and dp[j]
and s1[i - 1] == s3[i + j - 1]
)
from_s2 = (
j > 0
and dp[j - 1]
and s2[j - 1] == s3[i + j - 1]
)
dp[j] = from_s1 or from_s2
return dp[n]Space complexity becomes:
| Metric | Value |
|---|---|
| Time | O(m * n) |
| Space | O(n) |
Testing
def run_tests():
s = Solution()
assert s.isInterleave("aabcc", "dbbca", "aadbbcbcac") is True
assert s.isInterleave("aabcc", "dbbca", "aadbbbaccc") is False
assert s.isInterleave("", "", "") is True
assert s.isInterleave("", "abc", "abc") is True
assert s.isInterleave("abc", "", "abc") is True
assert s.isInterleave("abc", "def", "adbcef") is True
assert s.isInterleave("abc", "def", "abdecf") is True
assert s.isInterleave("abc", "def", "abdfec") is False
assert s.isInterleave("aa", "ab", "aaba") is True
assert s.isInterleave("aa", "ab", "abaa") is True
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
"aabcc", "dbbca", "aadbbcbcac" | Main true example |
"aabcc", "dbbca", "aadbbbaccc" | Main false example |
| Three empty strings | Empty base case |
Empty s1 | Must match only s2 |
Empty s2 | Must match only s1 |
| Mixed valid interleavings | Checks order preservation |
| Invalid order | Rejects broken source order |
| Repeated characters | Checks ambiguous choices |