Hard DSA — Advanced Dynamic Programming & Complex String Matching
Master complex state-transition dynamic programming, multi-dimensional memoization, wildcard matching algorithms, and advanced string evaluation patterns.
01. Edit Distance (Levenshtein Distance)
Given two strings word1 and word2, return the minimum number of operations required to convert word1 to word2 using insertions, deletions, or substitutions.
How It Works & Algorithmic Design
Solves using 2D Dynamic Programming Grid Matching:
- DP State: Define
dp[i][j]as the minimum edit distance betweenword1[0...i-1]andword2[0...j-1]. - Base Cases:
dp[i][0] = i(deleting all characters fromword1) anddp[0][j] = j(inserting all characters intoword1). - State Transitions:
- If
word1[i-1] === word2[j-1], no operation is needed:dp[i][j] = dp[i-1][j-1]. - Otherwise, take the minimum of 3 operations plus 1:
- Insert:
dp[i][j-1] + 1 - Delete:
dp[i-1][j] + 1 - Replace:
dp[i-1][j-1] + 1
- Insert:
- If
Why This Approach Works Best
Computes exact minimum edit distance in $O(m \times n)$ time and $O(m \times n)$ space, which can be space-optimized to $O(\min(m, n))$ using rolling arrays.
word1 = "horse", word2 = "ros"horse -> rorse (replace 'h' with 'r') -> rose (remove 'r') -> ros (remove 'e')3Answer: Edit Distance 2D DP implementation
function minDistance(word1, word2) {
const m = word1.length, n = word2.length;
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = 0; i <= m; i++) dp[i][0] = i;
for (let j = 0; j <= n; j++) dp[0][j] = j;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (word1[i - 1] === word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1;
}
}
}
return dp[m][n];
}- Minimizes structural distance across two sequences.
- Sub-problem operations:
Insert(left cell),Delete(top cell),Replace(diagonal cell). - Time complexity $O(m \times n)$.
02. Regular Expression Matching
Given an input string s and a pattern p, implement regular expression matching with support for '.' (matches any single character) and '*' (matches zero or more of the preceding element).
How It Works & Algorithmic Design
Solves using 2D Dynamic Programming with Wildcard Expansion:
- DP Table: Let
dp[i][j]be boolean matching status ofs[0...i-1]andp[0...j-1]. - Base Case:
dp[0][0] = true. Handle patterns likea*b*matching empty strings. - Matching Rules:
- If
p[j-1] === s[i-1]orp[j-1] === '.':dp[i][j] = dp[i-1][j-1]. - If
p[j-1] === '*':- Zero occurrences of preceding char:
dp[i][j] = dp[i][j-2]. - One/more occurrences (if preceding char matches
s[i-1]or is '.'):dp[i][j] = dp[i][j] || dp[i-1][j].
- Zero occurrences of preceding char:
- If
Why This Approach Works Best
Avoids infinite backtracking loops and evaluates complex regex state graphs in $O(|s| \times |p|)$ time and space.
s = "aa", p = "a*"'a*' expands to "aa", matching string s exactly.trueAnswer: Regular Expression Matching implementation
function isMatch(s, p) {
const m = s.length, n = p.length;
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
dp[0][0] = true;
for (let j = 2; j <= n; j += 2) {
if (p[j - 1] === '*') dp[0][j] = dp[0][j - 2];
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (p[j - 1] === '*' ) {
dp[i][j] = dp[i][j - 2] || (dp[i - 1][j] && (s[i - 1] === p[j - 2] || p[j - 2] === '.'));
} else {
dp[i][j] = dp[i - 1][j - 1] && (s[i - 1] === p[j - 1] || p[j - 1] === '.');
}
}
}
return dp[m][n];
}- Asterisk '*' operates on the preceding character.
- Two choices for '*': zero matches (
dp[i][j-2]) or extension match (dp[i-1][j]). - Optimal linear-squared complexity $O(|s| \times |p|)$.
03. Wildcard Matching
Given an input string s and a pattern p, implement wildcard pattern matching with support for '?' (matches any single character) and '*' (matches any sequence of characters, including empty sequence).
How It Works & Algorithmic Design
Solves using Greedy Two Pointers with Backtracking Positions:
- Pointers:
sIdx = 0,pIdx = 0,match = 0,starIdx = -1. - Traversal:
- Characters match or
p[pIdx] === '?': advance bothsIdx++,pIdx++. p[pIdx] === '*': save star checkpointstarIdx = pIdx,match = sIdx, advancepIdx++.- Mismatch after star: reset
pIdx = starIdx + 1, advancematch++, setsIdx = match. - Otherwise: mismatch without star $\to$ return
false.
- Characters match or
- Post Check: Skip trailing '*' in pattern. Return
sIdx === s.length && pIdx === p.length.
Why This Approach Works Best
Greedy two-pointer pointer tracking with star backtracking runs in $O(|s| \times |p|)$ worst-case and $O(1)$ constant space.
s = "cb", p = "?a"falseAnswer: Wildcard Matching using Greedy Star Pointers
function isMatch(s, p) {
let sIdx = 0, pIdx = 0, match = 0, starIdx = -1;
while (sIdx < s.length) {
if (pIdx < p.length && (p[pIdx] === '?' || s[sIdx] === p[pIdx])) {
sIdx++; pIdx++;
} else if (pIdx < p.length && p[pIdx] === '*') {
starIdx = pIdx; match = sIdx; pIdx++;
} else if (starIdx !== -1) {
pIdx = starIdx + 1; match++; sIdx = match;
} else return false;
}
while (pIdx < p.length && p[pIdx] === '*') pIdx++;
return pIdx === p.length;
}- Wildcard '*' matches any sequence without needing a predecessor.
- Greedy star position tracking achieves $O(1)$ space complexity.
- Significantly faster in practice compared to 2D DP table recursion.
04. Distinct Subsequences
Given two strings s and t, return the number of distinct subsequences of s which equals t.
How It Works & Algorithmic Design
Solves using 2D Subsequence Frequency Dynamic Programming:
- DP Definition:
dp[i][j]= number of distinct subsequences ofs[0...i-1]that matcht[0...j-1]. - Base Case:
dp[i][0] = 1(an empty stringtis a subsequence of anysprefix exactly once). - Transition:
- If
s[i-1] === t[j-1]:dp[i][j] = dp[i-1][j-1] + dp[i-1][j](sum of matching character choice plus skipping character choice). - Else:
dp[i][j] = dp[i-1][j](must skips[i-1]).
- If
Why This Approach Works Best
Evaluates total subsequence counts systematically in $O(|s| \times |t|)$ time and space.
s = "rabbbit", t = "rabbit"There are 3 distinct ways to drop one 'b' from "rabbbit" to leave "rabbit".3Answer: Distinct Subsequences DP implementation
function numDistinct(s, t) {
const m = s.length, n = t.length;
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = 0; i <= m; i++) dp[i][0] = 1;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (s[i - 1] === t[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[m][n];
}- Base case empty target
tgivesdp[i][0] = 1. - Matching characters allow choice between including or skipping
s[i-1]. - Time complexity is $O(|s| \times |t|)$.
05. Burst Balloons
You are given n balloons, indexed from 0 to n - 1. Each balloon is painted with a number on it represented by an array nums. If you burst balloon i you will get nums[i - 1] * nums[i] * nums[i + 1] coins. Return the maximum coins you can collect.
How It Works & Algorithmic Design
Solves using Reverse Interval Dynamic Programming:
- Padding: Pad input
numswith $1$ at both ends:[1, ...nums, 1]. - Reverse Decision: Think of picking the LAST balloon $k$ to burst in interval
[i, j]:- Coins gained from picking balloon $k$ last =
nums[i-1] * nums[k] * nums[j+1] + dp[i][k-1] + dp[k+1][j].
- Coins gained from picking balloon $k$ last =
- DP Interval Loop: Expand sub-array length
lenfrom $1$ to $n$, evaluating all choices of $k$ to maximize total coins.
Why This Approach Works Best
Reversing decision order makes sub-problems independent, solving the problem in $O(n^3)$ time and $O(n^2)$ space.
nums = [3, 1, 5, 8]Burst 1 -> Burst 5 -> Burst 3 -> Burst 8167Answer: Burst Balloons Interval DP implementation
function maxCoins(nums) {
const A = [1, ...nums, 1];
const n = A.length;
const dp = Array.from({ length: n }, () => new Array(n).fill(0));
for (let len = 1; len <= nums.length; len++) {
for (let i = 1; i <= nums.length - len + 1; i++) {
const j = i + len - 1;
for (let k = i; k <= j; k++) {
dp[i][j] = Math.max(dp[i][j], A[i - 1] * A[k] * A[j + 1] + dp[i][k - 1] + dp[k + 1][j]);
}
}
}
return dp[1][nums.length];
}- Reversing decision logic (selecting last burst balloon) keeps sub-problems decoupled.
- Pad boundary array with 1 at both index ends.
- Time complexity $O(n^3)$.
06. Minimum Window Substring
Given two strings s and t of lengths m and n respectively, return the minimum window substring of s such that every character in t (including duplicates) is included in the window.
How It Works & Algorithmic Design
Solves using Variable Sliding Window with Frequency Counts:
- Frequency Map: Build frequency target map for string
t. Maintainrequired = map.size. - Expand Right: Move
rightpointer overs:- Decrement character count in map. If count reaches zero, increment
formed++.
- Decrement character count in map. If count reaches zero, increment
- Contract Left: While
formed === required:- Update min window substring bounds if smaller.
- Shrink
leftpointer: increment map count. If count becomes $> 0$, decrementformed--.
Why This Approach Works Best
Achieves optimal $O(|s| + |t|)$ linear time complexity and $O(|s| + |t|)$ space without testing all $O(s^2)$ sub-string pairs.
s = "ADOBECODEBANC", t = "ABC"Window "BANC" contains 'A', 'B', 'C' with length 4."BANC"Answer: Minimum Window Substring implementation
function minWindow(s, t) {
if (!s || !t) return '';
const tCount = {};
for (const ch of t) tCount[ch] = (tCount[ch] || 0) + 1;
let required = Object.keys(tCount).length;
let left = 0, right = 0, formed = 0;
const windowCounts = {};
let ans = [-1, 0, 0];
while (right < s.length) {
const c = s[right];
windowCounts[c] = (windowCounts[c] || 0) + 1;
if (tCount[c] && windowCounts[c] === tCount[c]) formed++;
while (left <= right && formed === required) {
const cLeft = s[left];
if (ans[0] === -1 || right - left + 1 < ans[0]) ans = [right - left + 1, left, right];
windowCounts[cLeft]--;
if (tCount[cLeft] && windowCounts[cLeft] < tCount[cLeft]) formed--;
left++;
}
right++;
}
return ans[0] === -1 ? '' : s.substring(ans[1], ans[2] + 1);
}- Use
formedmatch counter to avoid $O(26)$ frequency comparison inside loops. - Two pointers advance linearly across input string.
- Linear time complexity $O(|s| + |t|)$.
07. Serialize and Deserialize Binary Tree
Design an algorithm to serialize and deserialize a binary tree into a string format and reconstruct the exact original tree structure.
How It Works & Algorithmic Design
Solves using Pre-Order Traversal with Sentinel Markers:
- Serialization: Perform DFS pre-order traversal. For each non-null node append
node.val + ','. For null nodes append'null,'. - Deserialization: Split serialized string by
','into a list/queue of tokens.- Read next token: if token equals
'null', returnnull. - Otherwise, create
node = new TreeNode(Number(token)). node.left = deserialize().node.right = deserialize().- Return
node.
- Read next token: if token equals
Why This Approach Works Best
Pre-order sentinel traversal uniquely identifies tree structure without needing both In-Order and Pre-Order arrays, executing in $O(n)$ time and $O(n)$ space.
[1, 2, 3, null, null, 4, 5]"1,2,null,null,3,4,null,null,5,null,null,"Reconstructed identical binary tree.Answer: Tree Serialization & Deserialization implementation
function serialize(root) {
if (!root) return 'null';
return `${root.val},${serialize(root.left)},${serialize(root.right)}`;
}
function deserialize(data) {
const nodes = data.split(',');
function buildTree() {
const val = nodes.shift();
if (val === 'null') return null;
const node = { val: Number(val), left: null, right: null };
node.left = buildTree();
node.right = buildTree();
return node;
}
return buildTree();
}- Sentinel null markers retain structural information during serialization.
- Pre-order token streams can be parsed using simple recursive queue consumers.
- Linear execution time $O(n)$.
08. Word Break II
Given a string s and a dictionary of strings wordDict, add spaces in s to construct a sentence where each word is a valid dictionary word. Return all such possible sentences in any order.
How It Works & Algorithmic Design
Solves using DFS Backtracking with Hash Map Memoization:
- Memoization Map:
memo[index]stores list of valid sentences formable froms[index...]. - DFS Function
dfs(start):- Base Case: if
start === s.length, return[""]. - If
startinmemo, returnmemo[start]. - Loop
endfromstart + 1tos.length:- Extract
word = s.substring(start, end). - If
wordDict.has(word):- Recurse
subSentences = dfs(end). - Append
word + (sub ? " " + sub : "")tores.
- Recurse
- Extract
- Base Case: if
- Return:
memo[0].
Why This Approach Works Best
Memoization prunes duplicate invalid search branches, preventing exponential catastrophic backtracking.
s = "catsanddog", wordDict = ["cat","cats","and","sand","dog"]["cats and dog", "cat sand dog"]["cats and dog", "cat sand dog"]Answer: Word Break II Memoized DFS implementation
function wordBreak(s, wordDict) {
const set = new Set(wordDict);
const memo = new Map();
function dfs(start) {
if (memo.has(start)) return memo.get(start);
if (start === s.length) return [''];
const res = [];
for (let end = start + 1; end <= s.length; end++) {
const word = s.substring(start, end);
if (set.has(word)) {
const subs = dfs(end);
for (const sub of subs) {
res.push(word + (sub ? ' ' + sub : ''));
}
}
}
memo.set(start, res);
return res;
}
return dfs(0);
}- Use Hash Set for $O(1)$ word existence lookups.
- Memoizing index search state cuts down repetitive sentence branching.
- Exponential worst-case complexity $O(2^n)$ when all combinations are valid.
09. Palindrome Partitioning II
Given a string s, partition s such that every substring of the partition is a palindrome. Return the minimum cuts needed for a palindrome partitioning of s.
How It Works & Algorithmic Design
Solves using 1D Dynamic Programming with Center Expansion Palindrome Pre-Check:
- Min Cut Table: Let
cuts[i]be the min cuts needed for substrings[0...i]. - Initialize:
cuts[i] = i(maximum cuts needed). - Center-Expansion Loop: For each center $i$ (both odd and even centers):
- Expand outward while
s[left] === s[right]:- If
left === 0,cuts[right] = 0(entire prefix is a palindrome). - Else,
cuts[right] = Math.min(cuts[right], cuts[left - 1] + 1).
- If
- Expand outward while
Why This Approach Works Best
Expanding from centers avoids creating $O(n^2)$ 2D boolean palindrome matrices, optimizing runtime to $O(n^2)$ time and $O(n)$ space.
s = "aab""aa" | "b" (1 cut).1Answer: Palindrome Partitioning II Center Expansion implementation
function minCut(s) {
const n = s.length;
const cuts = Array.from({ length: n }, (_, i) => i);
for (let center = 0; center < n; center++) {
expand(center, center);
expand(center, center + 1);
}
function expand(left, right) {
while (left >= 0 && right < n && s[left] === s[right]) {
cuts[right] = left === 0 ? 0 : Math.min(cuts[right], cuts[left - 1] + 1);
left--; right++;
}
}
return cuts[n - 1];
}- Center expansion tests both odd-length (
i, i) and even-length (i, i+1) palindromes. - Eliminates auxiliary 2D boolean palindrome memo tables.
- Executes in $O(n^2)$ time and $O(n)$ space.
10. Concatenated Words
Given an array of strings words (without duplicates), return all the concatenated words in the given list of words.
How It Works & Algorithmic Design
Solves using Word Length Sorting with Memoized Word Break Checking:
- Sort: Sort
wordsby length in ascending order. - Sub-word Verification: Maintain a set
wordSetof previously processed shorter words. - Word Break Check: For each word, execute DP
canForm(word, wordSet):- Check if
wordcan be partitioned into two or more words present inwordSet.
- Check if
- Update: Add current
wordintowordSet.
Why This Approach Works Best
Processing shorter words first ensures a word can only be formed by strictly shorter constituent parts, optimizing performance across large dictionaries.
["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"]"catsdogcats", "dogcatsdog", "ratcatdogcat"["catsdogcats", "dogcatsdog", "ratcatdogcat"]Answer: Concatenated Words implementation
function findAllConcatenatedWordsInADict(words) {
words.sort((a, b) => a.length - b.length);
const wordSet = new Set();
const res = [];
function canForm(word) {
if (wordSet.size === 0) return false;
const dp = new Array(word.length + 1).fill(false);
dp[0] = true;
for (let i = 1; i <= word.length; i++) {
for (let j = 0; j < i; j++) {
if (dp[j] && wordSet.has(word.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[word.length];
}
for (const word of words) {
if (canForm(word)) res.push(word);
wordSet.add(word);
}
return res;
}- Sorting words by length builds set incrementally.
- Reuses classic Word Break Dynamic Programming.
- Runs in $O(N \cdot L^2)$ time where $N$ is word count and $L$ is word length.