Given three strings s1, s2, and s3, determine whether s3 can be formed by interleaving the characters of s1 and s2while preserving each string's relative order. A classic 2D DP grid tracks reachability cell by cell.
You're given s1 = "aab", s2 = "axy", and s3 = "aaxaby". Is s3 an interleaving of s1 and s2? Yes — one valid split is: s1[0]+s2[0..1]+s1[1]+s2[2]+s1[2] → aaxayb = aaxayb… wait, let's be precise: take a from s1, ax from s2, a from s1, b from s1, y from s2 → aaxaby. The relative order within each source string is preserved. Return true. For s1="aabcc", s2="dbbca", s3="aadbbcbcac" → false.
dp[i][j] = "can s3[0..i+j) be formed by interleaving s1[0..i) and s2[0..j)". Each cell only depends on the cell directly above (we took a char from s1) or directly to the left (we took a char from s2). The answer is dp[m][n]. This turns an exponential branching search into an O(m·n) table fill.m + n ≠ |s3|, return false immediately — no interleaving can work.dp[0][0] = true — empty prefixes of s1 and s2 trivially form the empty prefix of s3.i=0, use only s2). dp[0][j] = dp[0][j-1] && s2[j-1] === s3[j-1]. We can only advance along s2 as long as every character matches.j=0, use only s1). Symmetric: check against s3[i-1].k = i + j - 1 is the s3 index to match. dp[i][j] = (dp[i-1][j] && s1[i-1]===s3[k]) || (dp[i][j-1] && s2[j-1]===s3[k]). "From above" means the last char came from s1; "from left" means it came from s2.dp[m][n].k = i + j - 1, not i or j. Many learners accidentally index s3 with i-1 or j-1 and get wrong answers on overlapping characters. Remember: after consuming i chars of s1 and j chars of s2, the next s3 position is i+j-1 (0-indexed).Because each row only reads from the row above, you can keep a single boolean[n+1] row and overwrite it in place, reducing space to O(n). The update becomes: dp[j] = (dp[j] && s1[i-1]===s3[k]) || (dp[j-1] && s2[j-1]===s3[k]).
aab, s2=axy, s3=aaxaby. Lengths sum correctly. Build dp[4][4] where dp[i][j] = can s3[0..i+j) be formed by interleaving s1[0..i) and s2[0..j).1▶function isInterleave(s1: string, s2: string, s3: string): boolean {2▶ const m = s1.length, n = s2.length;3 if (m + n !== s3.length) return false; // length precondition45 // dp[i][j] = can s3[0..i+j) be formed by interleaving s1[0..i) and s2[0..j)6▶ const dp: boolean[][] = Array.from({ length: m + 1 },7▶ () => new Array(n + 1).fill(false));89 dp[0][0] = true; // empty + empty = empty1011 for (let j = 1; j <= n; j++) // first row: use only s212 dp[0][j] = dp[0][j - 1] && s2[j - 1] === s3[j - 1];1314 for (let i = 1; i <= m; i++) // first col: use only s115 dp[i][0] = dp[i - 1][0] && s1[i - 1] === s3[i - 1];1617 for (let i = 1; i <= m; i++) {18 for (let j = 1; j <= n; j++) {19 const k = i + j - 1; // next char of s3 to match20 dp[i][j] =21 (dp[i - 1][j] && s1[i - 1] === s3[k]) || // take from s1 (match up)22 (dp[i][j - 1] && s2[j - 1] === s3[k]); // take from s2 (match left)23 }24 }2526 return dp[m][n];27}
function isInterleave(s1: string, s2: string, s3: string): boolean {
const m = s1.length, n = s2.length;
if (m + n !== s3.length) return false; // length precondition
// dp[i][j] = can s3[0..i+j) be formed by interleaving s1[0..i) and s2[0..j)
const dp: boolean[][] = Array.from({ length: m + 1 },
() => new Array(n + 1).fill(false));
dp[0][0] = true; // empty + empty = empty
for (let j = 1; j <= n; j++) // first row: use only s2
dp[0][j] = dp[0][j - 1] && s2[j - 1] === s3[j - 1];
for (let i = 1; i <= m; i++) // first col: use only s1
dp[i][0] = dp[i - 1][0] && s1[i - 1] === s3[i - 1];
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const k = i + j - 1; // next char of s3 to match
dp[i][j] =
(dp[i - 1][j] && s1[i - 1] === s3[k]) || // take from s1 (match up)
(dp[i][j - 1] && s2[j - 1] === s3[k]); // take from s2 (match left)
}
}
return dp[m][n];
}m + n !== s3.length, no interleaving is possible — the characters can't sum up. This single guard avoids all the work below.(m+1) × (n+1) grid of booleans, all false initially. The +1 accounts for the empty-prefix base cases.dp[0][0] = true — consuming zero chars from both s1 and s2 trivially forms the empty s3.i=0 we can only draw from s2. Each cell propagates only if the previous cell was reachable AND the next s2 character matches s3.j=0 we can only draw from s1.k = i + j - 1 is the next s3 position. The cell is true if we can legally arrive via the cell above (took s1[i-1]) or the cell to the left (took s2[j-1]).dp[m][n] answers whether all of s1 and all of s2 were consumed while matching all of s3.boolean[n+1] row updated in place gives O(n) space. The update rule is dp[j] = (dp[j] && s1[i-1]===s3[k]) || (dp[j-1] && s2[j-1]===s3[k]).dp[m][n] to reconstruct which source each character came from.dp[i][j][k] — same principle, O(l·m·n) time and space.s3 === s2 or s3 === s1), all-same characters (the count check passes but positions may not), s3 longer or shorter than s1+s2 (caught by the length guard).function isInterleave(s1: string, s2: string, s3: string): boolean {
const m = s1.length, n = s2.length;
if (m + n !== s3.length) return false; // length precondition
// dp[i][j] = can s3[0..i+j) be formed by interleaving s1[0..i) and s2[0..j)
const dp: boolean[][] = Array.from({ length: m + 1 },
() => new Array(n + 1).fill(false));
dp[0][0] = true; // empty + empty = empty
for (let j = 1; j <= n; j++) // first row: use only s2
dp[0][j] = dp[0][j - 1] && s2[j - 1] === s3[j - 1];
for (let i = 1; i <= m; i++) // first col: use only s1
dp[i][0] = dp[i - 1][0] && s1[i - 1] === s3[i - 1];
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const k = i + j - 1; // next char of s3 to match
dp[i][j] =
(dp[i - 1][j] && s1[i - 1] === s3[k]) || // take from s1 (match up)
(dp[i][j - 1] && s2[j - 1] === s3[k]); // take from s2 (match left)
}
}
return dp[m][n];
}At each step the next character of s3 must come from s1 or s2. Try both whenever they match and recurse; succeed when all three are exhausted.
function isInterleave(s1: string, s2: string, s3: string): boolean {
if (s1.length + s2.length !== s3.length) return false;
function rec(i: number, j: number): boolean {
const k = i + j;
if (k === s3.length) return true; // consumed everything
// take next char from s1
if (i < s1.length && s1[i] === s3[k] && rec(i + 1, j)) return true;
// take next char from s2
if (j < s2.length && s2[j] === s3[k] && rec(i, j + 1)) return true;
return false;
}
return rec(0, 0);
}(i, j) pair is revisited exponentially often. Memoising on (i, j)— or the iterative grid — collapses it to O(m·n).| "interleave / merge two strings preserving order" | 2D DP grid dp[i][j] |
| track progress through two sequences simultaneously | (m+1)×(n+1) boolean table |
| length check before anything | if m+n !== s3.length return false |
| "reduce space of 2D DP" | rolling 1D row — only needs previous row |
function isInterleave(s1: string, s2: string, s3: string): boolean {
const m = s1.length, n = s2.length;
if (m + n !== s3.length) return false;
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
dp[0][0] = true;
for (let j = 1; j <= n; j++) dp[0][j] = dp[0][j-1] && s2[j-1] === s3[j-1];
for (let i = 1; i <= m; i++) dp[i][0] = dp[i-1][0] && s1[i-1] === s3[i-1];
for (let i = 1; i <= m; i++)
for (let j = 1; j <= n; j++) {
const k = i + j - 1;
dp[i][j] = (dp[i-1][j] && s1[i-1] === s3[k]) ||
(dp[i][j-1] && s2[j-1] === s3[k]);
}
return dp[m][n];
}s3[0..i+j) be formed by interleaving s1[0..i) and s2[0..j)?"ab", s2="cd", s3="acbd". What is dp[1][1]?dp[i-1][j]) corresponds to: