String Interleaving
Overview
String Interleaving problems involve determining if a string can be formed by interleaving two other strings while maintaining the relative order of characters from each string. These problems test your understanding of dynamic programming, string manipulation, and state transitions.
Key challenges:
- Maintaining character order from both strings
- Handling overlapping characters
- Optimizing with memoization
- Space-efficient solutions
- Edge cases (empty strings, same characters)
Common interleaving problems include:
- Check if string is interleaving of two strings
- Find all interleaving combinations
- Interleaving with constraints
- Minimum operations to create interleaving
- Interleaving subsequences
Pattern 1: Basic String Interleaving
Check if s3 is formed by interleaving s1 and s2.
Dynamic Programming approach:
function isInterleave(s1: string, s2: string, s3: string): boolean {
const m = s1.length;
const n = s2.length;
if (m + n !== s3.length) return false;
const dp: boolean[][] = Array(m + 1)
.fill(null)
.map(() => Array(n + 1).fill(false));
dp[0][0] = true;
// Initialize first row (using only s2)
for (let j = 1; j <= n; j++) {
dp[0][j] = dp[0][j - 1] && s2[j - 1] === s3[j - 1];
}
// Initialize first column (using only s1)
for (let i = 1; i <= m; i++) {
dp[i][0] = dp[i - 1][0] && s1[i - 1] === s3[i - 1];
}
// Fill DP table
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
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]);
}
}
return dp[m][n];
}Time: O(m n), Space: O(m n)
Space-optimized (1D DP):
function isInterleaveOptimized(s1: string, s2: string, s3: string): boolean {
const m = s1.length;
const n = s2.length;
if (m + n !== s3.length) return false;
const dp: boolean[] = new Array(n + 1).fill(false);
dp[0] = true;
// Initialize first row
for (let j = 1; j <= n; j++) {
dp[j] = dp[j - 1] && s2[j - 1] === s3[j - 1];
}
// Fill DP table
for (let i = 1; i <= m; i++) {
dp[0] = dp[0] && s1[i - 1] === s3[i - 1];
for (let j = 1; j <= n; j++) {
dp[j] =
(dp[j] && s1[i - 1] === s3[i + j - 1]) ||
(dp[j - 1] && s2[j - 1] === s3[i + j - 1]);
}
}
return dp[n];
}Time: O(m * n), Space: O(min(m, n))
Pattern 2: Recursive with Memoization
Recursive approach with memoization.
function isInterleaveRecursive(s1: string, s2: string, s3: string): boolean {
const m = s1.length;
const n = s2.length;
if (m + n !== s3.length) return false;
const memo = new Map<string, boolean>();
const dfs = (i: number, j: number, k: number): boolean => {
if (k === s3.length) return true;
const key = `${i},${j}`;
if (memo.has(key)) return memo.get(key)!;
let result = false;
if (i < m && s1[i] === s3[k]) {
result = result || dfs(i + 1, j, k + 1);
}
if (j < n && s2[j] === s3[k]) {
result = result || dfs(i, j + 1, k + 1);
}
memo.set(key, result);
return result;
};
return dfs(0, 0, 0);
}Time: O(m n), Space: O(m n)
Pattern 3: Find All Interleavings
Generate all possible interleavings of two strings.
function findAllInterleavings(s1: string, s2: string): string[] {
const results: string[] = [];
const backtrack = (
current: string,
i: number,
j: number
): void => {
if (i === s1.length && j === s2.length) {
results.push(current);
return;
}
if (i < s1.length) {
backtrack(current + s1[i], i + 1, j);
}
if (j < s2.length) {
backtrack(current + s2[j], i, j + 1);
}
};
backtrack('', 0, 0);
return results;
}Time: O(2^(m+n)), Space: O(m + n)
With memoization for repeated subproblems:
function findAllInterleavingsMemoized(s1: string, s2: string): string[] {
const memo = new Map<string, string[]>();
const backtrack = (i: number, j: number): string[] => {
if (i === s1.length && j === s2.length) {
return [''];
}
const key = `${i},${j}`;
if (memo.has(key)) return memo.get(key)!;
const results: string[] = [];
if (i < s1.length) {
const subResults = backtrack(i + 1, j);
for (const sub of subResults) {
results.push(s1[i] + sub);
}
}
if (j < s2.length) {
const subResults = backtrack(i, j + 1);
for (const sub of subResults) {
results.push(s2[j] + sub);
}
}
memo.set(key, results);
return results;
};
return backtrack(0, 0);
}Pattern 4: Interleaving with Constraints
Interleaving with additional constraints.
Maximum consecutive characters from one string:
function isInterleaveWithMaxConsecutive(
s1: string,
s2: string,
s3: string,
maxConsecutive: number
): boolean {
const m = s1.length;
const n = s2.length;
if (m + n !== s3.length) return false;
const memo = new Map<string, boolean>();
const dfs = (
i: number,
j: number,
k: number,
consecutiveFrom: 's1' | 's2' | null,
consecutiveCount: number
): boolean => {
if (k === s3.length) return true;
const key = `${i},${j},${consecutiveFrom},${consecutiveCount}`;
if (memo.has(key)) return memo.get(key)!;
let result = false;
if (i < m && s1[i] === s3[k]) {
const newCount = consecutiveFrom === 's1' ? consecutiveCount + 1 : 1;
if (newCount <= maxConsecutive) {
result = result || dfs(i + 1, j, k + 1, 's1', newCount);
}
}
if (j < n && s2[j] === s3[k]) {
const newCount = consecutiveFrom === 's2' ? consecutiveCount + 1 : 1;
if (newCount <= maxConsecutive) {
result = result || dfs(i, j + 1, k + 1, 's2', newCount);
}
}
memo.set(key, result);
return result;
};
return dfs(0, 0, 0, null, 0);
}Minimum characters from each string:
function isInterleaveWithMinChars(
s1: string,
s2: string,
s3: string,
minFromS1: number,
minFromS2: number
): boolean {
const m = s1.length;
const n = s2.length;
if (m + n !== s3.length) return false;
const dp: boolean[][][] = Array(m + 1)
.fill(null)
.map(() => Array(n + 1)
.fill(null)
.map(() => Array(Math.max(minFromS1, minFromS2) + 1).fill(false)));
dp[0][0][0] = true;
for (let i = 0; i <= m; i++) {
for (let j = 0; j <= n; j++) {
for (let k = 0; k <= Math.max(minFromS1, minFromS2); k++) {
if (i + j === 0) continue;
if (i > 0 && s1[i - 1] === s3[i + j - 1]) {
dp[i][j][k] = dp[i][j][k] || dp[i - 1][j][Math.max(0, k - 1)];
}
if (j > 0 && s2[j - 1] === s3[i + j - 1]) {
dp[i][j][k] = dp[i][j][k] || dp[i][j - 1][k];
}
}
}
}
return dp[m][n][minFromS1] && dp[m][n][minFromS2];
}Pattern 5: Interleaving Count
Count number of ways to interleave two strings.
function interleaveCount(s1: string, s2: string): number {
const m = s1.length;
const n = s2.length;
const dp: number[][] = Array(m + 1)
.fill(null)
.map(() => Array(n + 1).fill(0));
dp[0][0] = 1;
// Initialize first row
for (let j = 1; j <= n; j++) {
dp[0][j] = 1;
}
// Initialize first column
for (let i = 1; i <= m; i++) {
dp[i][0] = 1;
}
// Fill DP table
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m][n];
}Time: O(m n), Space: O(m n)
Using combination formula:
function interleaveCountFormula(s1: string, s2: string): number {
const m = s1.length;
const n = s2.length;
// C(m+n, m) = (m+n)! / (m! * n!)
return factorial(m + n) / (factorial(m) * factorial(n));
}
function factorial(n: number): number {
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}Pattern 6: Interleaving with Wildcards
Handle interleaving when strings contain wildcards.
function isInterleaveWildcard(s1: string, s2: string, s3: string): boolean {
const m = s1.length;
const n = s2.length;
if (m + n !== s3.length) return false;
const dp: boolean[][] = Array(m + 1)
.fill(null)
.map(() => Array(n + 1).fill(false));
dp[0][0] = true;
// Initialize first row
for (let j = 1; j <= n; j++) {
const s2Char = s2[j - 1];
const s3Char = s3[j - 1];
dp[0][j] = dp[0][j - 1] && (s2Char === '?' || s3Char === '?' || s2Char === s3Char);
}
// Initialize first column
for (let i = 1; i <= m; i++) {
const s1Char = s1[i - 1];
const s3Char = s3[i - 1];
dp[i][0] = dp[i - 1][0] && (s1Char === '?' || s3Char === '?' || s1Char === s3Char);
}
// Fill DP table
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const s1Char = s1[i - 1];
const s2Char = s2[j - 1];
const s3Char = s3[i + j - 1];
const matchS1 = s1Char === '?' || s3Char === '?' || s1Char === s3Char;
const matchS2 = s2Char === '?' || s3Char === '?' || s2Char === s3Char;
dp[i][j] =
(dp[i - 1][j] && matchS1) ||
(dp[i][j - 1] && matchS2);
}
}
return dp[m][n];
}Pattern 7: Minimum Operations to Create Interleaving
Find minimum operations to make s3 an interleaving.
function minOperationsToInterleave(
s1: string,
s2: string,
s3: string
): number {
const m = s1.length;
const n = s2.length;
if (m + n < s3.length) return -1; // Impossible
const dp: number[][] = Array(m + 1)
.fill(null)
.map(() => Array(n + 1).fill(Infinity));
dp[0][0] = 0;
// Initialize first row
for (let j = 1; j <= n; j++) {
dp[0][j] = j; // All insertions from s2
}
// Initialize first column
for (let i = 1; i <= m; i++) {
dp[i][0] = i; // All insertions from s1
}
// Fill DP table
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const k = i + j - 1;
if (k < s3.length) {
if (s1[i - 1] === s3[k]) {
dp[i][j] = Math.min(dp[i][j], dp[i - 1][j]);
}
if (s2[j - 1] === s3[k]) {
dp[i][j] = Math.min(dp[i][j], dp[i][j - 1]);
}
// Mismatch - need operation
dp[i][j] = Math.min(dp[i][j], dp[i - 1][j] + 1, dp[i][j - 1] + 1);
}
}
}
return dp[m][n] === Infinity ? -1 : dp[m][n];
}Pattern 8: Interleaving Subsequences
Check if s3 contains interleaving subsequences.
function isInterleaveSubsequence(s1: string, s2: string, s3: string): boolean {
const m = s1.length;
const n = s2.length;
const k = s3.length;
if (m + n > k) return false;
const dp: boolean[][] = Array(m + 1)
.fill(null)
.map(() => Array(n + 1).fill(false));
dp[0][0] = true;
// Initialize first row
for (let j = 1; j <= n; j++) {
dp[0][j] = dp[0][j - 1] && findChar(s2[j - 1], s3, j - 1) !== -1;
}
// Initialize first column
for (let i = 1; i <= m; i++) {
dp[i][0] = dp[i - 1][0] && findChar(s1[i - 1], s3, i - 1) !== -1;
}
// Fill DP table
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const pos = i + j - 1;
if (pos < k) {
dp[i][j] =
(dp[i - 1][j] && s1[i - 1] === s3[pos]) ||
(dp[i][j - 1] && s2[j - 1] === s3[pos]);
}
}
}
return dp[m][n];
}
function findChar(char: string, s: string, start: number): number {
for (let i = start; i < s.length; i++) {
if (s[i] === char) return i;
}
return -1;
}Pattern 9: Interleaving with Priority
Interleaving where one string has priority.
function isInterleaveWithPriority(
s1: string,
s2: string,
s3: string,
priority: 's1' | 's2'
): boolean {
const m = s1.length;
const n = s2.length;
if (m + n !== s3.length) return false;
const memo = new Map<string, boolean>();
const dfs = (i: number, j: number, k: number): boolean => {
if (k === s3.length) return true;
const key = `${i},${j}`;
if (memo.has(key)) return memo.get(key)!;
let result = false;
// Try priority string first
if (priority === 's1' && i < m && s1[i] === s3[k]) {
result = result || dfs(i + 1, j, k + 1);
}
if (priority === 's2' && j < n && s2[j] === s3[k]) {
result = result || dfs(i, j + 1, k + 1);
}
// Then try other string
if (priority === 's1' && j < n && s2[j] === s3[k]) {
result = result || dfs(i, j + 1, k + 1);
}
if (priority === 's2' && i < m && s1[i] === s3[k]) {
result = result || dfs(i + 1, j, k + 1);
}
memo.set(key, result);
return result;
};
return dfs(0, 0, 0);
}Pattern 10: Interleaving Validation
Validate if interleaving maintains character frequencies.
function isValidInterleaving(s1: string, s2: string, s3: string): boolean {
if (s1.length + s2.length !== s3.length) return false;
// Check character frequencies
const freq1 = getFrequency(s1);
const freq2 = getFrequency(s2);
const freq3 = getFrequency(s3);
// Merge frequencies from s1 and s2
const merged = new Map<string, number>();
for (const [char, count] of freq1) {
merged.set(char, (merged.get(char) || 0) + count);
}
for (const [char, count] of freq2) {
merged.set(char, (merged.get(char) || 0) + count);
}
// Compare with s3
if (merged.size !== freq3.size) return false;
for (const [char, count] of merged) {
if (freq3.get(char) !== count) return false;
}
// Check if interleaving is possible
return isInterleave(s1, s2, s3);
}
function getFrequency(s: string): Map<string, number> {
const freq = new Map<string, number>();
for (const char of s) {
freq.set(char, (freq.get(char) || 0) + 1);
}
return freq;
}Pattern 11: Interleaving with Replacement
Interleaving where characters can be replaced.
function isInterleaveWithReplacement(
s1: string,
s2: string,
s3: string,
maxReplacements: number
): boolean {
const m = s1.length;
const n = s2.length;
if (m + n !== s3.length) return false;
const dp: number[][] = Array(m + 1)
.fill(null)
.map(() => Array(n + 1).fill(Infinity));
dp[0][0] = 0;
// Initialize first row
for (let j = 1; j <= n; j++) {
dp[0][j] = s2[j - 1] === s3[j - 1] ? dp[0][j - 1] : dp[0][j - 1] + 1;
}
// Initialize first column
for (let i = 1; i <= m; i++) {
dp[i][0] = s1[i - 1] === s3[i - 1] ? dp[i - 1][0] : dp[i - 1][0] + 1;
}
// Fill DP table
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const k = i + j - 1;
// From s1
const cost1 = s1[i - 1] === s3[k] ? 0 : 1;
dp[i][j] = Math.min(dp[i][j], dp[i - 1][j] + cost1);
// From s2
const cost2 = s2[j - 1] === s3[k] ? 0 : 1;
dp[i][j] = Math.min(dp[i][j], dp[i][j - 1] + cost2);
}
}
return dp[m][n] <= maxReplacements;
}Pattern 12: Interleaving Path Reconstruction
Reconstruct the actual interleaving path.
function interleavePath(s1: string, s2: string, s3: string): string[] | null {
const m = s1.length;
const n = s2.length;
if (m + n !== s3.length) return null;
const dp: boolean[][] = Array(m + 1)
.fill(null)
.map(() => Array(n + 1).fill(false));
const parent: Array<Array<'s1' | 's2' | null>> = Array(m + 1)
.fill(null)
.map(() => Array(n + 1).fill(null));
dp[0][0] = true;
// Initialize and fill DP
for (let j = 1; j <= n; j++) {
if (s2[j - 1] === s3[j - 1]) {
dp[0][j] = true;
parent[0][j] = 's2';
}
}
for (let i = 1; i <= m; i++) {
if (s1[i - 1] === s3[i - 1]) {
dp[i][0] = true;
parent[i][0] = 's1';
}
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (dp[i - 1][j] && s1[i - 1] === s3[i + j - 1]) {
dp[i][j] = true;
parent[i][j] = 's1';
} else if (dp[i][j - 1] && s2[j - 1] === s3[i + j - 1]) {
dp[i][j] = true;
parent[i][j] = 's2';
}
}
}
if (!dp[m][n]) return null;
// Reconstruct path
const path: string[] = [];
let i = m, j = n;
while (i > 0 || j > 0) {
if (parent[i][j] === 's1') {
path.unshift(`s1[${i - 1}]=${s1[i - 1]}`);
i--;
} else if (parent[i][j] === 's2') {
path.unshift(`s2[${j - 1}]=${s2[j - 1]}`);
j--;
} else {
break;
}
}
return path;
}When to Use Interleaving Techniques
Use interleaving algorithms when:
- Problem mentions "interleave", "interweave", or "merge"
- Need to check if string is interleaving
- Maintain order from both strings
- Find all combinations of interleaving
- Handle constraints on interleaving
- Optimize with DP or memoization
Template (DP-based)
function isInterleaveTemplate(s1: string, s2: string, s3: string): boolean {
const m = s1.length;
const n = s2.length;
if (m + n !== s3.length) return false;
const dp: boolean[][] = Array(m + 1)
.fill(null)
.map(() => Array(n + 1).fill(false));
dp[0][0] = true;
// Initialize boundaries
// Fill DP table
return dp[m][n];
}Template (Recursive with Memo)
function isInterleaveRecursiveTemplate(s1: string, s2: string, s3: string): boolean {
const memo = new Map<string, boolean>();
const dfs = (i: number, j: number, k: number): boolean => {
if (k === s3.length) return true;
const key = `${i},${j}`;
if (memo.has(key)) return memo.get(key)!;
let result = false;
if (i < s1.length && s1[i] === s3[k]) {
result = result || dfs(i + 1, j, k + 1);
}
if (j < s2.length && s2[j] === s3[k]) {
result = result || dfs(i, j + 1, k + 1);
}
memo.set(key, result);
return result;
};
return dfs(0, 0, 0);
}Time and Space Complexity Summary
- Basic interleaving: O(m n) time, O(m n) space, O(min(m, n)) optimized
- All interleavings: O(2^(m+n)) time, O(m + n) space
- With constraints: O(m n k) time where k is constraint parameter
- Count: O(m n) time, O(m n) space
- Path reconstruction: O(m n) time, O(m n) space
Practice Tips
- Use DP — For checking if interleaving is possible
- Use backtracking — For finding all combinations
- Memoization — Essential for recursive solutions
- Space optimization — Use 1D DP when possible
- Handle edge cases — Empty strings, length mismatch
- State transitions — Track indices carefully
Common Mistakes
- Index errors — Careful with i+j-1 for s3 index
- Not initializing — First row and column
- Wrong state — dp[i][j] represents using i chars from s1, j from s2
- Missing memoization — Causes TLE in recursion
- Length check — Always verify m + n === s3.length first
Related Concepts
- Dynamic Programming — Core technique for interleaving
- String Manipulation — Basic string operations
- Backtracking — For finding all combinations
- Edit Distance — Similar DP structure
String interleaving problems are common in interviews. Master these patterns, and you'll be well-prepared for string combination and DP-related questions.