97. Interleaving String
Problem Description
You are given three strings s1
, s2
, and s3
. You need to determine whether s3
can be formed by interleaving s1
and s2
.
An interleaving of two strings s
and t
means:
- Both strings are divided into multiple non-empty substrings
s
is divided inton
substrings:s = s₁ + s₂ + ... + sₙ
t
is divided intom
substrings:t = t₁ + t₂ + ... + tₘ
- The difference between the number of substrings must satisfy:
|n - m| ≤ 1
- The substrings are alternately combined to form the interleaved string in one of these patterns:
s₁ + t₁ + s₂ + t₂ + s₃ + t₃ + ...
t₁ + s₁ + t₂ + s₂ + t₃ + s₃ + ...
In simpler terms, you need to check if s3
can be formed by taking characters from s1
and s2
while:
- Maintaining the relative order of characters from each original string
- Using all characters from both
s1
ands2
exactly once - The resulting string equals
s3
For example:
- If
s1 = "aab"
,s2 = "axy"
, ands3 = "aaxaby"
, thens3
is a valid interleaving because we can form it by taking characters alternately while preserving order:a
from s1,a
from s1,x
from s2,a
from s2,b
from s1,y
from s2.
The concatenation operator +
simply means joining strings together.
Intuition
Think of this problem as making choices at each step: when building s3
, we need to decide whether the current character comes from s1
or s2
.
Imagine you have two pointers, one for s1
and one for s2
, starting at the beginning of each string. At any point, you're trying to match the current position in s3
. The key insight is that the position in s3
is always the sum of how far you've progressed in both s1
and s2
- if you've used i
characters from s1
and j
characters from s2
, you're at position i + j
in s3
.
At each step, you have at most two choices:
- If the current character in
s1
matches the current position ins3
, you can choose to take froms1
and move its pointer forward - If the current character in
s2
matches the current position ins3
, you can choose to take froms2
and move its pointer forward
This naturally leads to a recursive solution where we explore both possibilities when available. The base case is when we've used all characters from both s1
and s2
- if we've also matched all of s3
, then we've found a valid interleaving.
The first optimization is obvious: if the total length of s1
and s2
doesn't equal the length of s3
, it's impossible to form an interleaving, so we can return false
immediately.
The second optimization comes from recognizing that we might encounter the same subproblem multiple times - the same (i, j)
pair might be reached through different paths. For instance, taking character 1 from s1
then character 1 from s2
leads to the same state as taking character 1 from s2
then character 1 from s1
. This overlap in subproblems makes memoization effective, where we cache the result for each (i, j)
pair to avoid redundant calculations.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses memoization search (also known as top-down dynamic programming) to efficiently solve the problem.
Implementation Details:
-
Initial Check: First, we check if
len(s1) + len(s2) == len(s3)
. If not, it's impossible to forms3
froms1
ands2
, so we returnfalse
immediately. -
Recursive Function with Memoization: We define a function
dfs(i, j)
where:i
represents the current index in strings1
j
represents the current index in strings2
- The function returns
true
if we can form the remaining part ofs3
starting from positioni + j
using characters froms1[i:]
ands2[j:]
-
Base Case: When
i >= m
andj >= n
(wherem = len(s1)
andn = len(s2)
), we've used all characters from both strings, so we returntrue
. -
Recursive Cases: At each step, we calculate
k = i + j
, which represents our current position ins3
. Then we check:- If
i < m
ands1[i] == s3[k]
: We can take the character froms1
. We recursively calldfs(i + 1, j)
to check if the rest can form a valid interleaving. If it returnstrue
, we returntrue
. - If
j < n
ands2[j] == s3[k]
: We can take the character froms2
. We recursively calldfs(i, j + 1)
to check if the rest can form a valid interleaving. If it returnstrue
, we returntrue
. - If neither condition is met, we return
false
.
- If
-
Memoization: The
@cache
decorator automatically memoizes the results ofdfs(i, j)
. This prevents redundant calculations when the same state(i, j)
is reached through different paths, reducing time complexity from exponential toO(m × n)
.
Why This Works:
The algorithm explores all possible ways to interleave s1
and s2
. At each position in s3
, it tries to match the current character with either the current character in s1
or s2
(if they match), then recursively checks if the remaining characters can form a valid interleaving. The memoization ensures that each unique state is computed only once, making the solution efficient.
Time Complexity: O(m × n)
where m
and n
are the lengths of s1
and s2
respectively, since we have at most m × n
unique states.
Space Complexity: O(m × n)
for the memoization cache plus O(m + n)
for the recursion stack depth.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with s1 = "ab"
, s2 = "cd"
, and s3 = "acbd"
.
Step 1: Initial Check
- Length check:
len(s1) + len(s2) = 2 + 2 = 4 = len(s3)
✓ - We can proceed with the algorithm.
Step 2: Start DFS from (0, 0)
We'll trace through the recursive calls. Each state is represented as dfs(i, j)
where i
is the index in s1
and j
is the index in s2
.
dfs(0, 0): k = 0+0 = 0, need to match s3[0] = 'a' - s1[0] = 'a' matches s3[0] ✓ → try dfs(1, 0) - s2[0] = 'c' doesn't match s3[0] ✗ dfs(1, 0): k = 1+0 = 1, need to match s3[1] = 'c' - s1[1] = 'b' doesn't match s3[1] ✗ - s2[0] = 'c' matches s3[1] ✓ → try dfs(1, 1) dfs(1, 1): k = 1+1 = 2, need to match s3[2] = 'b' - s1[1] = 'b' matches s3[2] ✓ → try dfs(2, 1) - s2[1] = 'd' doesn't match s3[2] ✗ dfs(2, 1): k = 2+1 = 3, need to match s3[3] = 'd' - i = 2 >= len(s1), can't use s1 - s2[1] = 'd' matches s3[3] ✓ → try dfs(2, 2) dfs(2, 2): Base case reached! - i = 2 >= len(s1) = 2 ✓ - j = 2 >= len(s2) = 2 ✓ - Return true
Step 3: Backtrack with Results
The recursive calls return true
all the way back:
dfs(2, 2)
returnstrue
dfs(2, 1)
returnstrue
(because taking 'd' from s2 worked)dfs(1, 1)
returnstrue
(because taking 'b' from s1 worked)dfs(1, 0)
returnstrue
(because taking 'c' from s2 worked)dfs(0, 0)
returnstrue
(because taking 'a' from s1 worked)
Result: The function returns true
, confirming that "acbd" is a valid interleaving of "ab" and "cd".
The characters were taken in this order:
- 'a' from s1 (position 0)
- 'c' from s2 (position 0)
- 'b' from s1 (position 1)
- 'd' from s2 (position 1)
This forms "acbd" while maintaining the relative order of characters from both strings.
Solution Implementation
1class Solution:
2 def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
3 """
4 Determine if s3 is formed by interleaving of s1 and s2.
5
6 Args:
7 s1: First string
8 s2: Second string
9 s3: Target string to check if it's an interleaving of s1 and s2
10
11 Returns:
12 True if s3 is an interleaving of s1 and s2, False otherwise
13 """
14 from functools import cache
15
16 # Get lengths of input strings
17 len_s1, len_s2 = len(s1), len(s2)
18
19 # Early termination: if lengths don't match, s3 cannot be an interleaving
20 if len_s1 + len_s2 != len(s3):
21 return False
22
23 @cache
24 def dfs(index_s1: int, index_s2: int) -> bool:
25 """
26 Recursively check if remaining portions can form valid interleaving.
27
28 Args:
29 index_s1: Current index in string s1
30 index_s2: Current index in string s2
31
32 Returns:
33 True if remaining portions can form valid interleaving
34 """
35 # Base case: reached end of both strings
36 if index_s1 >= len_s1 and index_s2 >= len_s2:
37 return True
38
39 # Calculate current index in s3
40 index_s3 = index_s1 + index_s2
41
42 # Try taking character from s1 if possible
43 if index_s1 < len_s1 and s1[index_s1] == s3[index_s3]:
44 if dfs(index_s1 + 1, index_s2):
45 return True
46
47 # Try taking character from s2 if possible
48 if index_s2 < len_s2 and s2[index_s2] == s3[index_s3]:
49 if dfs(index_s1, index_s2 + 1):
50 return True
51
52 # No valid path found
53 return False
54
55 # Start DFS from beginning of both strings
56 return dfs(0, 0)
57
1class Solution {
2 // Memoization cache: stores results for (index1, index2) pairs
3 private Map<List<Integer>, Boolean> memo = new HashMap<>();
4
5 // Input strings
6 private String s1;
7 private String s2;
8 private String s3;
9
10 // Lengths of s1 and s2
11 private int length1;
12 private int length2;
13
14 /**
15 * Determines if s3 is formed by interleaving s1 and s2.
16 *
17 * @param s1 First string to interleave
18 * @param s2 Second string to interleave
19 * @param s3 Target string to check
20 * @return true if s3 can be formed by interleaving s1 and s2, false otherwise
21 */
22 public boolean isInterleave(String s1, String s2, String s3) {
23 length1 = s1.length();
24 length2 = s2.length();
25
26 // Early termination: if lengths don't match, interleaving is impossible
27 if (length1 + length2 != s3.length()) {
28 return false;
29 }
30
31 // Store strings as instance variables for DFS access
32 this.s1 = s1;
33 this.s2 = s2;
34 this.s3 = s3;
35
36 // Start DFS from beginning of both strings
37 return dfs(0, 0);
38 }
39
40 /**
41 * Recursive DFS helper to check if remaining portions can form valid interleaving.
42 *
43 * @param index1 Current index in s1
44 * @param index2 Current index in s2
45 * @return true if remaining portions can form valid interleaving, false otherwise
46 */
47 private boolean dfs(int index1, int index2) {
48 // Base case: reached end of both strings successfully
49 if (index1 >= length1 && index2 >= length2) {
50 return true;
51 }
52
53 // Check memoization cache
54 List<Integer> key = List.of(index1, index2);
55 if (memo.containsKey(key)) {
56 return memo.get(key);
57 }
58
59 // Calculate current position in s3
60 int index3 = index1 + index2;
61 boolean result = false;
62
63 // Try taking character from s1 if possible and it matches s3
64 if (index1 < length1 && s1.charAt(index1) == s3.charAt(index3)) {
65 result = dfs(index1 + 1, index2);
66 }
67
68 // Try taking character from s2 if s1 didn't work and s2 matches s3
69 if (!result && index2 < length2 && s2.charAt(index2) == s3.charAt(index3)) {
70 result = dfs(index1, index2 + 1);
71 }
72
73 // Cache and return result
74 memo.put(key, result);
75 return result;
76 }
77}
78
1class Solution {
2public:
3 bool isInterleave(string s1, string s2, string s3) {
4 int len1 = s1.size();
5 int len2 = s2.size();
6
7 // Early termination: if lengths don't match, s3 cannot be formed
8 if (len1 + len2 != s3.size()) {
9 return false;
10 }
11
12 // Memoization table: -1 = unvisited, 0 = false, 1 = true
13 // memo[i][j] represents whether s3[i+j:] can be formed by interleaving s1[i:] and s2[j:]
14 vector<vector<int>> memo(len1 + 1, vector<int>(len2 + 1, -1));
15
16 // Recursive function to check if interleaving is possible
17 function<bool(int, int)> canInterleave = [&](int idx1, int idx2) -> bool {
18 // Base case: both strings are fully consumed
19 if (idx1 >= len1 && idx2 >= len2) {
20 return true;
21 }
22
23 // Return memoized result if already computed
24 if (memo[idx1][idx2] != -1) {
25 return memo[idx1][idx2] == 1;
26 }
27
28 // Initialize current state as false
29 memo[idx1][idx2] = 0;
30
31 // Current position in s3
32 int idx3 = idx1 + idx2;
33
34 // Try taking character from s1 if possible
35 if (idx1 < len1 && s1[idx1] == s3[idx3]) {
36 if (canInterleave(idx1 + 1, idx2)) {
37 memo[idx1][idx2] = 1;
38 return true;
39 }
40 }
41
42 // Try taking character from s2 if possible
43 if (idx2 < len2 && s2[idx2] == s3[idx3]) {
44 if (canInterleave(idx1, idx2 + 1)) {
45 memo[idx1][idx2] = 1;
46 return true;
47 }
48 }
49
50 // Return the result (will be false if neither option worked)
51 return memo[idx1][idx2] == 1;
52 };
53
54 // Start recursion from the beginning of both strings
55 return canInterleave(0, 0);
56 }
57};
58
1/**
2 * Determines if s3 is formed by interleaving s1 and s2
3 * @param s1 - First string to interleave
4 * @param s2 - Second string to interleave
5 * @param s3 - Target string to check
6 * @returns true if s3 can be formed by interleaving s1 and s2, false otherwise
7 */
8function isInterleave(s1: string, s2: string, s3: string): boolean {
9 const s1Length: number = s1.length;
10 const s2Length: number = s2.length;
11
12 // Early return if combined length doesn't match target
13 if (s1Length + s2Length !== s3.length) {
14 return false;
15 }
16
17 // Memoization table: 0 = unvisited, 1 = valid path, -1 = invalid path
18 // memo[i][j] represents the result for s1[i:] and s2[j:] matching s3[i+j:]
19 const memo: number[][] = new Array(s1Length + 1)
20 .fill(0)
21 .map(() => new Array(s2Length + 1).fill(0));
22
23 /**
24 * DFS helper function to check if interleaving is possible from given positions
25 * @param s1Index - Current index in s1
26 * @param s2Index - Current index in s2
27 * @returns true if valid interleaving exists from current position
28 */
29 const dfs = (s1Index: number, s2Index: number): boolean => {
30 // Base case: reached end of both strings
31 if (s1Index >= s1Length && s2Index >= s2Length) {
32 return true;
33 }
34
35 // Return memoized result if already computed
36 if (memo[s1Index][s2Index] !== 0) {
37 return memo[s1Index][s2Index] === 1;
38 }
39
40 // Mark current state as invalid initially
41 memo[s1Index][s2Index] = -1;
42
43 // Try taking character from s1 if possible
44 if (s1Index < s1Length &&
45 s1[s1Index] === s3[s1Index + s2Index] &&
46 dfs(s1Index + 1, s2Index)) {
47 memo[s1Index][s2Index] = 1;
48 }
49
50 // Try taking character from s2 if s1 path didn't work
51 if (memo[s1Index][s2Index] === -1 &&
52 s2Index < s2Length &&
53 s2[s2Index] === s3[s1Index + s2Index] &&
54 dfs(s1Index, s2Index + 1)) {
55 memo[s1Index][s2Index] = 1;
56 }
57
58 // Return whether a valid path was found
59 return memo[s1Index][s2Index] === 1;
60 };
61
62 // Start DFS from beginning of both strings
63 return dfs(0, 0);
64}
65
Time and Space Complexity
The time complexity is O(m × n)
, where m
is the length of string s1
and n
is the length of string s2
. This is because the recursive function dfs(i, j)
can have at most m × n
unique states (with i
ranging from 0
to m
and j
ranging from 0
to n
). Due to the @cache
decorator, each state is computed only once, and subsequent calls to the same state return the cached result immediately.
The space complexity is O(m × n)
. This accounts for two factors:
- The cache storage used by the
@cache
decorator, which stores up tom × n
states - The recursive call stack depth, which in the worst case can go up to
O(m + n)
when we traverse through all characters of eithers1
ors2
before backtracking
Since O(m × n)
dominates O(m + n)
, the overall space complexity is O(m × n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting the Length Check
One of the most common mistakes is diving straight into the recursive solution without first checking if the total length matches. This can lead to unnecessary computation or incorrect results.
Pitfall Example:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
@cache
def dfs(i, j):
# Directly start recursion without length check
if i >= len(s1) and j >= len(s2):
return True
# ... rest of the code
return dfs(0, 0)
Why it's wrong: If len(s1) + len(s2) ≠ len(s3)
, the function might still return True
when it reaches the end of s1 and s2, even though we haven't consumed all of s3 (or have consumed too much).
Solution: Always verify len(s1) + len(s2) == len(s3)
before starting the recursion.
2. Incorrect Index Calculation for s3
Another frequent error is incorrectly tracking the position in s3 or using a separate index that can get out of sync.
Pitfall Example:
@cache
def dfs(i, j, k): # Adding a third parameter for s3 index
if i >= len(s1) and j >= len(s2):
return k >= len(s3)
if i < len(s1) and s1[i] == s3[k]:
if dfs(i + 1, j, k + 1): # Manually incrementing k
return True
# ... rest of the code
Why it's wrong: Adding an extra parameter increases complexity and memory usage. Since k
is always equal to i + j
, it's redundant and can lead to bugs if not maintained correctly.
Solution: Always calculate k = i + j
within the function rather than passing it as a parameter.
3. Missing Memoization
Without memoization, the solution becomes exponentially slow for larger inputs due to repeated calculations of the same states.
Pitfall Example:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
def dfs(i, j): # No @cache decorator
if i >= len(s1) and j >= len(s2):
return True
# ... rest of the code
return dfs(0, 0)
Why it's wrong: Without memoization, the time complexity becomes O(2^(m+n)) in the worst case, causing timeout for larger inputs.
Solution: Use @cache
decorator or implement manual memoization with a dictionary.
4. Incorrect Boundary Checks
Using wrong comparison operators or checking bounds after accessing the array can cause index out of range errors.
Pitfall Example:
@cache
def dfs(i, j):
if i == len(s1) and j == len(s2): # Using == instead of >=
return True
k = i + j
# Checking bounds AFTER accessing the character
if s1[i] == s3[k] and i < len(s1): # Wrong order!
if dfs(i + 1, j):
return True
Why it's wrong:
- Using
==
instead of>=
might miss the base case if indices somehow exceed the length - Accessing
s1[i]
before checkingi < len(s1)
causes IndexError
Solution: Always check bounds before accessing array elements, and use >=
for safety in base case.
5. Not Handling Empty Strings
The solution might fail or behave unexpectedly with empty strings if not properly considered.
Pitfall Example:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
if not s1 and not s2 and not s3: # Only checking if all are empty
return True
# ... rest of the code
Why it's wrong: This doesn't handle cases where only one or two strings are empty. For example, if s1 = ""
, s2 = "abc"
, s3 = "abc"
, this should return True
.
Solution: The length check len(s1) + len(s2) == len(s3)
naturally handles empty strings correctly, and the recursive solution works fine with empty strings as long as the length check passes.
Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.
Which of the following method should we use to solve this problem?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!