Facebook Pixel

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 into n substrings: s = s₁ + s₂ + ... + sₙ
  • t is divided into m 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 and s2 exactly once
  • The resulting string equals s3

For example:

  • If s1 = "aab", s2 = "axy", and s3 = "aaxaby", then s3 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. If the current character in s1 matches the current position in s3, you can choose to take from s1 and move its pointer forward
  2. If the current character in s2 matches the current position in s3, you can choose to take from s2 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:

  1. Initial Check: First, we check if len(s1) + len(s2) == len(s3). If not, it's impossible to form s3 from s1 and s2, so we return false immediately.

  2. Recursive Function with Memoization: We define a function dfs(i, j) where:

    • i represents the current index in string s1
    • j represents the current index in string s2
    • The function returns true if we can form the remaining part of s3 starting from position i + j using characters from s1[i:] and s2[j:]
  3. Base Case: When i >= m and j >= n (where m = len(s1) and n = len(s2)), we've used all characters from both strings, so we return true.

  4. Recursive Cases: At each step, we calculate k = i + j, which represents our current position in s3. Then we check:

    • If i < m and s1[i] == s3[k]: We can take the character from s1. We recursively call dfs(i + 1, j) to check if the rest can form a valid interleaving. If it returns true, we return true.
    • If j < n and s2[j] == s3[k]: We can take the character from s2. We recursively call dfs(i, j + 1) to check if the rest can form a valid interleaving. If it returns true, we return true.
    • If neither condition is met, we return false.
  5. Memoization: The @cache decorator automatically memoizes the results of dfs(i, j). This prevents redundant calculations when the same state (i, j) is reached through different paths, reducing time complexity from exponential to O(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 Evaluator

Example 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) returns true
  • dfs(2, 1) returns true (because taking 'd' from s2 worked)
  • dfs(1, 1) returns true (because taking 'b' from s1 worked)
  • dfs(1, 0) returns true (because taking 'c' from s2 worked)
  • dfs(0, 0) returns true (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:

  1. The cache storage used by the @cache decorator, which stores up to m × n states
  2. The recursive call stack depth, which in the worst case can go up to O(m + n) when we traverse through all characters of either s1 or s2 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 checking i < 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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More