Given strings s1, s2, and s3, find whether s3 is formed by an interleaving of s1 and s2.
An interleaving of two strings s and t is a configuration where s and t are divided into n and msubstrings respectively, such that:
s = s1 + s2 + ... + sn
t = t1 + t2 + ... + tm
|n - m| <= 1
The interleaving is s1 + t1 + s2 + t2 + s3 + t3 + ... or t1 + s1 + t2 + s2 + t3 + s3 + ...
Note:a + b is the concatenation of strings a and b.
Example 1:
Input: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
Output: true
Explanation: One way to obtain s3 is:
Split s1 into s1 = "aa" + "bc" + "c", and s2 into s2 = "dbbc" + "a".
Interleaving the two splits, we get "aa" + "dbbc" + "bc" + "a" + "c" = "aadbbcbcac".
Since s3 can be obtained by interleaving s1 and s2, we return true.
Example 2:
Input: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
Output: false
Explanation: Notice how it is impossible to interleave s2 with any other string to obtain s3.
Example 3:
Input: s1 = "", s2 = "", s3 = ""
Output: true
Constraints:
0 <= s1.length, s2.length <= 100
0 <= s3.length <= 200
s1, s2, and s3 consist of lowercase English letters.
Follow up: Could you solve it using only O(s2.length) additional memory space?
Approaches
4 approaches with complexity analysis and trade-offs.
This approach uses a simple recursive function to explore all possible ways of forming s3 by picking characters from s1 and s2. It directly models the decision process: at each character of s3, we decide whether to match it with the current character of s1 or s2. If a character in s3 matches both, we explore both possibilities recursively.
Algorithm
The core idea is to use a recursive function, say check(i, j), which determines if the rest of s3 (from index i+j) can be formed by the rest of s1 (from index i) and the rest of s2 (from index j).
First, perform a preliminary check: if s1.length() + s2.length() != s3.length(), it's impossible to form s3, so return false.
Base Case: If we have successfully traversed all of s1 and s2 (i.e., i == s1.length() and j == s2.length()), it means we have successfully formed s3. Return true.
Recursive Step: At the current state (i, j), we are trying to match s3.charAt(i+j).
If s1.charAt(i) matches s3.charAt(i+j), we make a recursive call check(i + 1, j). If this call returns true, we have found a valid interleaving.
If s2.charAt(j) matches s3.charAt(i+j), we make a recursive call check(i, j + 1). If this call returns true, we have also found a valid interleaving.
The result for check(i, j) is true if either of the possible recursive calls returns true.
If neither character matches or if the subsequent recursive calls return false, this path is invalid.
Walkthrough
The method involves a helper function that takes two pointers, i for s1 and j for s2, indicating the number of characters already used from each string. The current character to be matched in s3 is at index k = i + j.
If the current character of s3 matches the character at s1[i], we recursively call the function with i+1. If it matches s2[j], we recursively call with j+1. If it matches both, we try both paths and return true if either path leads to a solution. The recursion stops when we have successfully used all characters from both s1 and s2.
1public class Solution {2 public boolean isInterleave(String s1, String s2, String s3) {3 if (s1.length() + s2.length() != s3.length()) {4 return false;5 }6 return check(s1, s2, s3, 0, 0);7 }89 private boolean check(String s1, String s2, String s3, int i, int j) {10 // Base case: if we have reached the end of all strings11 if (i == s1.length() && j == s2.length()) {12 return true;13 }1415 boolean match1 = false;16 if (i < s1.length() && s1.charAt(i) == s3.charAt(i + j)) {17 match1 = check(s1, s2, s3, i + 1, j);18 }1920 // If the first path was successful, no need to check the second21 if (match1) {22 return true;23 }2425 boolean match2 = false;26 if (j < s2.length() && s2.charAt(j) == s3.charAt(i + j)) {27 match2 = check(s1, s2, s3, i, j + 1);28 }2930 return match2;31 }32}
Complexity
Time
O(2^(m+n))
Space
O(m + n)
Trade-offs
Pros
Simple to understand and implement.
Directly translates the problem definition into code.
Cons
Extremely inefficient due to a large number of redundant computations for the same subproblems (i.e., the same i and j values are computed multiple times through different recursive paths).
Will likely result in a 'Time Limit Exceeded' error on most online judges for non-trivial inputs.
The brute-force approach suffers from re-calculating the same subproblems multiple times. We can optimize this by using memoization, a top-down dynamic programming technique. We store the result of each unique state (i, j) in a 2D array. When the recursive function is called for a state that has already been computed, we simply return the stored result instead of re-computing it.
Algorithm
First, check if s1.length() + s2.length() != s3.length(). If not, return false.
Create a 2D memoization table, memo, of size (s1.length() + 1) x (s2.length() + 1) to store the results of subproblems. Initialize it with a value indicating that the state has not been computed (e.g., -1 or null).
Use a recursive helper function check(i, j) as in the brute-force approach.
Memoization Check: Before computing check(i, j), check if memo[i][j] has already been computed. If so, return the stored value.
Recursive Step: If the result is not in the memo table, compute it as in the brute-force approach.
Store Result: Before returning the computed result, store it in memo[i][j] for future use.
Walkthrough
We enhance the recursive solution by adding a 2D array, memo, to cache the results. memo[i][j] will store whether an interleaving is possible for s1's suffix starting at i and s2's suffix starting at j. We can use an integer array where 1 means true, 0 means false, and -1 (or another sentinel value) means not computed.
This avoids the exponential explosion of recursive calls by ensuring that each subproblem check(i, j) is solved only once. The time complexity is reduced to the number of states multiplied by the work done per state, which is constant.
1public class Solution {2 public boolean isInterleave(String s1, String s2, String s3) {3 if (s1.length() + s2.length() != s3.length()) {4 return false;5 }6 // -1: not computed, 0: false, 1: true7 int[][] memo = new int[s1.length() + 1][s2.length() + 1];8 for (int[] row : memo) {9 java.util.Arrays.fill(row, -1);10 }11 return check(s1, s2, s3, 0, 0, memo) == 1;12 }1314 private int check(String s1, String s2, String s3, int i, int j, int[][] memo) {15 if (i == s1.length() && j == s2.length()) {16 return 1;17 }18 if (memo[i][j] != -1) {19 return memo[i][j];20 }2122 boolean match1 = false;23 if (i < s1.length() && s1.charAt(i) == s3.charAt(i + j)) {24 if (check(s1, s2, s3, i + 1, j, memo) == 1) {25 match1 = true;26 }27 }2829 if (match1) {30 memo[i][j] = 1;31 return 1;32 }3334 boolean match2 = false;35 if (j < s2.length() && s2.charAt(j) == s3.charAt(i + j)) {36 if (check(s1, s2, s3, i, j + 1, memo) == 1) {37 match2 = true;38 }39 }4041 memo[i][j] = match2 ? 1 : 0;42 return memo[i][j];43 }44}
Complexity
Time
O(m * n)
Space
O(m * n)
Trade-offs
Pros
Drastically improves time complexity by eliminating redundant computations.
Guaranteed to run in polynomial time.
Relatively easy to convert from the brute-force recursive solution.
Cons
Requires O(m*n) extra space for the memoization table.
While much faster, it still uses recursion, which might have a slight overhead compared to a purely iterative solution.
This approach converts the top-down memoized recursion into a bottom-up iterative solution. We use a 2D DP table where dp[i][j] represents whether the first i characters of s1 and the first j characters of s2 can form an interleaving of the first i+j characters of s3. We fill this table iteratively, starting from the base case dp[0][0], until we compute the final answer dp[s1.length()][s2.length()].
Algorithm
First, check the length constraint: s1.length() + s2.length() != s3.length().
Create a 2D boolean DP table dp of size (m+1) x (n+1), where m=s1.length() and n=s2.length().
dp[i][j] will be true if s3's prefix of length i+j is an interleaving of s1's prefix of length i and s2's prefix of length j.
Base Case:dp[0][0] = true (two empty strings form an empty string).
Fill First Row:dp[0][j] is true if s2's prefix matches s3's prefix. dp[0][j] = dp[0][j-1] && s2.charAt(j-1) == s3.charAt(j-1).
Fill First Column:dp[i][0] is true if s1's prefix matches s3's prefix. dp[i][0] = dp[i-1][0] && s1.charAt(i-1) == s3.charAt(i-1).
Fill the Rest: For dp[i][j], check the character s3.charAt(i+j-1).
If it matches s1.charAt(i-1), we can potentially form the string if dp[i-1][j] is true.
If it matches s2.charAt(j-1), we can potentially form the string if dp[i][j-1] is true.
The state dp[i][j] holds a boolean value. The table is of size (m+1) x (n+1). We build the solution from smaller subproblems to larger ones.
dp[i][j] is true if either of these conditions holds:
The i-th character of s1 (i.e., s1[i-1]) matches the (i+j)-th character of s3 (i.e., s3[i+j-1]), AND the subproblem dp[i-1][j] is true.
The j-th character of s2 (i.e., s2[j-1]) matches the (i+j)-th character of s3 (i.e., s3[i+j-1]), AND the subproblem dp[i][j-1] is true.
By filling the table row by row or column by column, we can compute the final result at dp[m][n].
1public class Solution {2 public boolean isInterleave(String s1, String s2, String s3) {3 int m = s1.length(), n = s2.length();4 if (m + n != s3.length()) {5 return false;6 }78 boolean[][] dp = new boolean[m + 1][n + 1];910 for (int i = 0; i <= m; i++) {11 for (int j = 0; j <= n; j++) {12 if (i == 0 && j == 0) {13 dp[i][j] = true;14 } else if (i == 0) {15 dp[i][j] = dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(j - 1);16 } else if (j == 0) {17 dp[i][j] = dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i - 1);18 } else {19 dp[i][j] = (dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i + j - 1)) ||20 (dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1));21 }22 }23 }2425 return dp[m][n];26 }27}
Complexity
Time
O(m * n)
Space
O(m * n)
Trade-offs
Pros
Avoids recursion and potential stack overflow issues.
Often slightly more performant than memoization due to no recursion overhead.
The logic is systematic and easy to follow.
Cons
Requires O(m*n) space, which might be large for the given constraints and is not the most optimal solution.
This is the most optimized approach, addressing the follow-up question. By observing the 2D DP transition dp[i][j] = f(dp[i-1][j], dp[i][j-1]), we see that to compute the current row i of the DP table, we only need the values from the previous row i-1. This allows us to reduce the space complexity from O(m*n) to O(n) (or O(min(m,n))) by using only a single 1D array to store the previous row's results while computing the current row.
Algorithm
First, check the length constraint. If s1.length() + s2.length() != s3.length(), return false.
To optimize space, we can ensure s2 is the shorter string by swapping s1 and s2 if needed.
Create a 1D boolean DP array dp of size (s2.length() + 1).
dp[j] will correspond to dp[i][j] in the 2D version. The outer loop will iterate through i (for s1) and the inner loop through j (for s2).
Initialization (for i=0):
dp[0] = true.
For j from 1 to n, dp[j] = dp[j-1] && s2.charAt(j-1) == s3.charAt(j-1).
Here, dp[j] on the right side refers to the value from the previous row (i-1), and dp[j-1] refers to the already updated value in the current row (i).
The final answer is the last element of the dp array, dp[n].
Walkthrough
We can optimize the 2D DP solution's space by using a 1D array, say dp, of size n+1 where n is the length of s2. dp[j] will store the result for dp[i][j]. As we iterate through i (from 1 to m), we update this dp array. For each i, the new dp[j] is calculated based on the old dp[j] (which corresponds to dp[i-1][j]) and the new dp[j-1] (which corresponds to dp[i][j-1]). This way, we effectively simulate filling the 2D table but only ever store one row at a time.
1public class Solution {2 public boolean isInterleave(String s1, String s2, String s3) {3 int m = s1.length(), n = s2.length();4 if (m + n != s3.length()) {5 return false;6 }78 // To optimize space, we want the 1D array to be of the smaller size.9 if (m < n) {10 return isInterleave(s2, s1, s3);11 }1213 boolean[] dp = new boolean[n + 1];14 dp[0] = true;1516 // Initialize first row (i=0)17 for (int j = 1; j <= n; j++) {18 dp[j] = dp[j - 1] && s2.charAt(j - 1) == s3.charAt(j - 1);19 }2021 // Fill the rest of the rows22 for (int i = 1; i <= m; i++) {23 // Update first column element for current row i24 dp[0] = dp[0] && s1.charAt(i - 1) == s3.charAt(i - 1);25 for (int j = 1; j <= n; j++) {26 // dp[j] is from previous row (i-1), dp[j-1] is from current row (i)27 dp[j] = (dp[j] && s1.charAt(i - 1) == s3.charAt(i + j - 1)) ||28 (dp[j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1));29 }30 }3132 return dp[n];33 }34}
Complexity
Time
O(m * n)
Space
O(n)
Trade-offs
Pros
Optimal space complexity, using only O(n) additional space, where n is the length of the shorter string.
Maintains the efficient O(m*n) time complexity.
Directly answers the follow-up question in the problem description.
Cons
The logic for updating the 1D array in place can be slightly more complex to reason about compared to the 2D DP approach.
Solutions
Solution
1public class Solution {2 public bool IsInterleave(string s1, string s2, string s3) {3 int m = s1.Length, n = s2.Length;4 if (m + n != s3.Length) {5 return false;6 }7 bool[] f = new bool[n + 1];8 f[0] = true;9 for (int i = 0; i <= m; ++i) {10 for (int j = 0; j <= n; ++j) {11 int k = i + j - 1;12 if (i > 0) {13 f[j] &= s1[i - 1] == s3[k];14 }15 if (j > 0) {16 f[j] |= (f[j - 1] & s2[j - 1] == s3[k]);17 }18 }19 }20 return f[n];21 }22}
1class Solution {2public3 boolean isInterleave(String s1, String s2, String s3) {4 int m = s1.length(), n = s2.length();5 if (m + n != s3.length()) {6 return false;7 }8 boolean[] f = new boolean[n + 1];9 f[0] = true;10 for (int i = 0; i <= m; ++i) {11 for (int j = 0; j <= n; ++j) {12 int k = i + j - 1;13 if (i > 0) {14 f[j] &= s1.charAt(i - 1) == s3.charAt(k);15 }16 if (j > 0) {17 f[j] |= (f[j - 1] & s2.charAt(j - 1) == s3.charAt(k));18 }19 }20 }21 return f[n];22 }23}
1class Solution {2public:3 bool isInterleave(string s1, string s2, string s3) {4 int m = s1.size(), n = s2.size();5 if (m + n != s3.size()) {6 return false;7 }8 bool f[n + 1];9 memset(f, false, sizeof(f));10 f[0] = true;11 for (int i = 0; i <= m; ++i) {12 for (int j = 0; j <= n; ++j) {13 int k = i + j - 1;14 if (i) {15 f[j] &= s1[i - 1] == s3[k];16 }17 if (j) {18 f[j] |= (s2[j - 1] == s3[k] && f[j - 1]);19 }20 }21 }22 return f[n];23 }24};
1# 2-d class Solution : def isInterleave ( self , s1 : str , s2 : str , s3 : str ) -> bool : if len ( s1 ) + len ( s2 ) != len ( s3 ): return False dp = [[ False ] * ( len ( s2 ) + 1 ) for _ in range ( len ( s1 ) + 1 )] dp [ 0 ][ 0 ] = True for i in range ( 1 , len ( s1 ) + 1 ): dp [ i ][ 0 ] = s1 [ i - 1 ] == s3 [ i - 1 ] and dp [ i - 1 ][ 0 ] for i in range ( 1 , len ( s2 ) + 1 ): dp [ 0 ][ i ] = s2 [ i - 1 ] == s3 [ i - 1 ] and dp [ 0 ][ i - 1 ] # fill in dp array for i in range ( 1 , len ( s1 ) + 1 ): for j in range ( 1 , len ( s2 ) + 1 ): with_s1 = s1 [ i - 1 ] == s3 [ i + j - 1 ] and dp [ i - 1 ][ j ] with_s2 = s2 [ j - 1 ] == s3 [ i + j - 1 ] and dp [ i ][ j - 1 ] dp [ i ][ j ] = with_s1 or with_s2 return dp [ - 1 ][ - 1 ] ############ # 1-d class Solution : def isInterleave ( self , s1 : str , s2 : str , s3 : str ) -> bool : m , n = len ( s1 ), len ( s2 ) if m + n != len ( s3 ): return False f = [ True ] + [ False ] * n for i in range ( m + 1 ): for j in range ( n + 1 ): k = i + j - 1 if i : f [ j ] &= s1 [ i - 1 ] == s3 [ k ] if j : f [ j ] |= f [ j - 1 ] and s2 [ j - 1 ] == s3 [ k ] return f [ n ] ########### class Solution : # dfs def isInterleave ( self , s1 : str , s2 : str , s3 : str ) -> bool : m , n = len ( s1 ), len ( s2 ) if m + n != len ( s3 ): return False @ cache def dfs ( i , j ): if i == m and j == n : return True return ( ( i < m and s1 [ i ] == s3 [ i + j ] and dfs ( i + 1 , j ) ) or ( j < n and s2 [ j ] == s3 [ i + j ] and dfs ( i , j + 1 ) ) ) return dfs ( 0 , 0 )
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.