116.Interleaving String
Problem
Given strings s1, s2 and s3, determine whether s3 is formed by an interleaving of s1 and s2.
An interleaving splits s1 and s2 into pieces and merges them alternately while keeping the characters of each string in their original order. Equivalently: can you read s3 left to right, assigning each character to either s1 or s2, so that the characters assigned to s1 spell s1 and those assigned to s2 spell s2?
Examples
Input: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
Output: true
Explanation: "aa" + "dbbc" + "bc" + "a" + "c" → s1 gives aa|bc|c, s2 gives dbbc|a.Input: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
Output: falseInput: s1 = "", s2 = "", s3 = ""
Output: trueConstraints
0 <= s1.length, s2.length <= 1000 <= s3.length <= 200s1,s2,s3consist of lowercase English letters.
💡 Key Insight
If we have used i characters of s1 and j of s2, the next character of s3 is forced to be s3[i + j] — so the state is just (i, j). From there, we can advance in s1 if s1[i] == s3[i + j], or in s2 if s2[j] == s3[i + j]. Greedy fails when both match, so we need DP over the (m+1) x (n+1) grid, which compresses to one row. Always check m + n == s3.length() first.
Recursion (Brute Force)
TimeO(2^(m+n))SpaceO(m + n)Intuition
dfs(i, j) answers: can s3[i+j..] be formed from s1[i..] and s2[j..]? Try taking the next character from s1 and, if that fails, from s2.
Algorithm
- If
m + n != s3.length(), returnfalse. dfs(i, j):- If
i == mandj == n, returntrue. k = i + j.- If
i < mands1[i] == s3[k]anddfs(i + 1, j), returntrue. - If
j < nands2[j] == s3[k]anddfs(i, j + 1), returntrue. - Return
false.
- If
Visualization
flowchart TD
A["dfs(0,0)<br/>s1='ab' s2='ac' s3='acab'"] -->|"s1 'a'"| B["dfs(1,0)<br/>need 'c'"]
A -->|"s2 'a'"| C["dfs(0,1)<br/>need 'c'"]
B -->|"s1 'b' != 'c'"| D["fail"]
B -->|"s2 'a' != 'c'"| E["fail"]
C -->|"s2 'c'"| F["dfs(0,2)<br/>need 'a'"]
F -->|"s1 'a'"| G["dfs(1,2)<br/>need 'b'"]
G -->|"s1 'b'"| H["dfs(2,2) = true"]Both strings start with 'a', so a greedy choice (always take from s1) would fail; we must explore both branches.
Code
class Solution {
public boolean isInterleave(String s1, String s2, String s3) {
if (s1.length() + s2.length() != s3.length()) {
return false;
}
return dfs(s1, s2, s3, 0, 0);
}
private boolean dfs(String s1, String s2, String s3, int i, int j) {
if (i == s1.length() && j == s2.length()) {
return true; // consumed everything
}
int k = i + j; // next char of s3 to match
if (i < s1.length() && s1.charAt(i) == s3.charAt(k) && dfs(s1, s2, s3, i + 1, j)) {
return true;
}
return j < s2.length() && s2.charAt(j) == s3.charAt(k) && dfs(s1, s2, s3, i, j + 1);
}
}Complexity
- Time: O(2^(m+n)) — two branches at each of up to
m + nsteps. - Space: O(m + n) — recursion depth.
Top-Down Memoization
TimeO(m · n)SpaceO(m · n)Intuition
Many different branch orders reach the same (i, j). Memoize the boolean result for each pair.
Algorithm
memo = new Boolean[m + 1][n + 1].- Same recursion; return
memo[i][j]if set; store the result before returning.
Dry Run
s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
dfs(0,0) → s1 'a' → dfs(1,0) → s1 'a' → dfs(2,0) → s2 'd' → dfs(2,1) → ...
when a branch fails at, say, dfs(3,3), memo[3][3] = false
any later path arriving at (3,3) returns false immediatelyCode
class Solution {
private Boolean[][] memo;
private String s1, s2, s3;
public boolean isInterleave(String s1, String s2, String s3) {
if (s1.length() + s2.length() != s3.length()) {
return false;
}
this.s1 = s1;
this.s2 = s2;
this.s3 = s3;
memo = new Boolean[s1.length() + 1][s2.length() + 1];
return dfs(0, 0);
}
private boolean dfs(int i, int j) {
if (i == s1.length() && j == s2.length()) {
return true;
}
if (memo[i][j] != null) {
return memo[i][j];
}
int k = i + j;
boolean ok = (i < s1.length() && s1.charAt(i) == s3.charAt(k) && dfs(i + 1, j))
|| (j < s2.length() && s2.charAt(j) == s3.charAt(k) && dfs(i, j + 1));
memo[i][j] = ok;
return ok;
}
}Complexity
- Time: O(m · n) — each
(i, j)evaluated once. - Space: O(m · n) — memo plus O(m + n) stack.
Bottom-Up 2-D Tabulation
TimeO(m · n).SpaceO(m · n).Intuition
Let dp[i][j] be true if the prefixes s1[0..i) and s2[0..j) interleave to form s3[0..i+j). The last character s3[i+j-1] came either from s1 or from s2.
Recurrence:
dp[i][j] = (dp[i-1][j] && s1[i-1] == s3[i+j-1]) || (dp[i][j-1] && s2[j-1] == s3[i+j-1]), with dp[0][0] = true.
Algorithm
- Length check.
dp = new boolean[m + 1][n + 1];dp[0][0] = true.- Fill row by row with the recurrence (skip the
i-1term on row 0 and thej-1term on column 0). - Return
dp[m][n].
Dry Run
s1 = "aabcc" (rows), s2 = "dbbca" (columns), s3 = "aadbbcbcac":
| "" | d | b | b | c | a | |
|---|---|---|---|---|---|---|
| "" | T | F | F | F | F | F |
| a | T | F | F | F | F | F |
| a | T | T | T | T | T | F |
| b | F | T | T | F | T | F |
| c | F | F | T | T | T | T |
| c | F | F | F | T | F | T |
Code
class Solution {
public boolean isInterleave(String s1, String s2, String s3) {
int m = s1.length(), n = s2.length();
if (m + n != s3.length()) {
return false;
}
boolean[][] dp = new boolean[m + 1][n + 1];
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
if (i == 0 && j == 0) {
dp[i][j] = true; // empty + empty = empty
continue;
}
char c = s3.charAt(i + j - 1);
boolean fromS1 = i > 0 && dp[i - 1][j] && s1.charAt(i - 1) == c;
boolean fromS2 = j > 0 && dp[i][j - 1] && s2.charAt(j - 1) == c;
dp[i][j] = fromS1 || fromS2;
}
}
return dp[m][n];
}
}Complexity
- Time: O(m · n).
- Space: O(m · n).
Space-Optimized 1-D DPOptimal
TimeO(m · n)SpaceO(n)Intuition
Each cell needs only the cell above (previous row, same column) and the cell to the left (current row). A single boolean array dp[j] works: before updating, dp[j] is "above"; dp[j-1] was just updated and is "left".
Algorithm
- Length check;
dp = new boolean[n + 1]. - For
i = 0..m, forj = 0..n:- If
i == 0 && j == 0:dp[0] = true. - Else
dp[j] = (i > 0 && dp[j] && s1[i-1] == s3[i+j-1]) || (j > 0 && dp[j-1] && s2[j-1] == s3[i+j-1]).
- If
- Return
dp[n].
Dry Run
flowchart TD
A["i=0: dp = [T,F,F,F,F,F]"] -->|"s1[0]='a' matches s3[0]"| B["i=1: dp = [T,F,F,F,F,F]"]
B -->|"s1[1]='a', then s2 'dbbc' fills left-to-right"| C["i=2: dp = [T,T,T,T,T,F]"]
C -->|"s1[2]='b'"| D["i=3: dp = [F,T,T,F,T,F]"]
D -->|"s1[3]='c', s2[4]='a' at s3[8]"| E["i=4: dp = [F,F,T,T,T,T]"]
E -->|"s1[4]='c' at s3[9]"| F["i=5: dp = [F,F,F,T,F,T]"]
F --> G["dp[5] = true"]Code
class Solution {
public boolean isInterleave(String s1, String s2, String s3) {
int m = s1.length(), n = s2.length();
if (m + n != s3.length()) {
return false;
}
boolean[] dp = new boolean[n + 1];
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
if (i == 0 && j == 0) {
dp[0] = true;
continue;
}
char c = s3.charAt(i + j - 1);
boolean fromS1 = i > 0 && dp[j] && s1.charAt(i - 1) == c; // dp[j] = above
boolean fromS2 = j > 0 && dp[j - 1] && s2.charAt(j - 1) == c; // dp[j-1] = left
dp[j] = fromS1 || fromS2;
}
}
return dp[n];
}
}Complexity
- Time: O(m · n) — each cell once.
- Space: O(n) — one row (swap the strings to make it O(min(m, n)) if desired).
Complexity Comparison
| Approach | Time | Space |
|---|---|---|
| Recursion (Brute Force) | O(2^(m+n)) | O(m + n) |
| Top-Down Memoization | O(m · n) | O(m · n) |
| Bottom-Up 2-D Tabulation | O(m · n) | O(m · n) |
| Space-Optimized 1-D DP | O(m · n) | O(n) |
Common Pitfalls
- Skipping the
m + n == s3.length()check — the DP can then read past the end ofs3or returntruefor a too-longs3. - Greedy two-pointer matching: when
s1[i]ands2[j]both equals3[k], you cannot know which to take without exploring both. - Using a third index
kas part of the state — it is alwaysi + j, so a 3-D table is wasted memory. - In the 1-D version, forgetting that
dp[j]must be recomputed forj = 0on every row (it may turnfalse). - Empty strings:
s1 = ""reduces tos2 == s3; make sure your bounds checks allowm = 0orn = 0.