🧠
Top-Down DP with Memoization
💡 Memoization avoids recomputing results for the same substring start indices, drastically improving performance while keeping the recursive structure.
Intuition
Store results of subproblems (start indices) so that each substring is solved once.
Algorithm
- Initialize a memo dictionary to store results for start indices.
- Recursively check prefixes as before.
- Before recursion, check if result for start is memoized.
- Return memoized result if available, else compute and store it.
💡 Memoization prunes the recursion tree by caching results, turning exponential time into polynomial.
Recurrence:f(i) = OR over j in [i+1, n] of (s[i:j] in wordDict AND f(j)) with memoization
def wordBreak(s, wordDict):
word_set = set(wordDict)
memo = {}
def backtrack(start):
if start == len(s):
return True
if start in memo:
return memo[start]
for end in range(start + 1, len(s) + 1):
if s[start:end] in word_set and backtrack(end):
memo[start] = True
return True
memo[start] = False
return False
# Driver code
if __name__ == '__main__':
s = "leetcode"
wordDict = ["leet", "code"]
print(wordBreak(s, wordDict))
Line Notes
memo = {}Initialize memo dictionary to cache results
if start in memo:Return cached result if available to avoid recomputation
memo[start] = TrueCache positive result before returning
memo[start] = FalseCache negative result before returning
for end in range(start + 1, len(s) + 1):Try all prefixes from start
import java.util.*;
public class WordBreak {
public static boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordSet = new HashSet<>(wordDict);
Map<Integer, Boolean> memo = new HashMap<>();
return backtrack(s, 0, wordSet, memo);
}
private static boolean backtrack(String s, int start, Set<String> wordSet, Map<Integer, Boolean> memo) {
if (start == s.length()) return true;
if (memo.containsKey(start)) return memo.get(start);
for (int end = start + 1; end <= s.length(); end++) {
if (wordSet.contains(s.substring(start, end)) && backtrack(s, end, wordSet, memo)) {
memo.put(start, true);
return true;
}
}
memo.put(start, false);
return false;
}
public static void main(String[] args) {
String s = "leetcode";
List<String> wordDict = Arrays.asList("leet", "code");
System.out.println(wordBreak(s, wordDict));
}
}
Line Notes
Map<Integer, Boolean> memo = new HashMap<>();Memo map caches results for start indices
if (memo.containsKey(start)) return memo.get(start);Return cached result if present
memo.put(start, true);Cache positive result
memo.put(start, false);Cache negative result
for (int end = start + 1; end <= s.length(); end++)Try all prefixes
#include <iostream>
#include <unordered_set>
#include <unordered_map>
#include <vector>
using namespace std;
bool backtrack(const string &s, int start, const unordered_set<string> &wordSet, unordered_map<int, bool> &memo) {
if (start == s.size()) return true;
if (memo.count(start)) return memo[start];
for (int end = start + 1; end <= (int)s.size(); ++end) {
if (wordSet.count(s.substr(start, end - start)) && backtrack(s, end, wordSet, memo)) {
memo[start] = true;
return true;
}
}
memo[start] = false;
return false;
}
int main() {
string s = "leetcode";
vector<string> wordDict = {"leet", "code"};
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
unordered_map<int, bool> memo;
cout << (backtrack(s, 0, wordSet, memo) ? "true" : "false") << endl;
return 0;
}
Line Notes
unordered_map<int, bool> memo;Memo map caches results for start indices
if (memo.count(start)) return memo[start];Return cached result if available
memo[start] = true;Cache positive result
memo[start] = false;Cache negative result
for (int end = start + 1; end <= (int)s.size(); ++end)Try all prefixes
function wordBreak(s, wordDict) {
const wordSet = new Set(wordDict);
const memo = new Map();
function backtrack(start) {
if (start === s.length) return true;
if (memo.has(start)) return memo.get(start);
for (let end = start + 1; end <= s.length; end++) {
if (wordSet.has(s.substring(start, end)) && backtrack(end)) {
memo.set(start, true);
return true;
}
}
memo.set(start, false);
return false;
}
return backtrack(0);
}
// Test
console.log(wordBreak("leetcode", ["leet", "code"]));
Line Notes
const memo = new Map();Memo map caches results for start indices
if (memo.has(start)) return memo.get(start);Return cached result if present
memo.set(start, true);Cache positive result
memo.set(start, false);Cache negative result
for (let end = start + 1; end <= s.length; end++)Try all prefixes
TimeO(n^3) worst case due to substring extraction and checking all partitions
SpaceO(n) recursion stack + O(n) memo storage
Memoization ensures each start index is computed once, but substring operations cost O(n), leading to cubic time in worst case.
💡 For n=20, this reduces calls from millions to at most 20, but substring checks still add overhead.
Interview Verdict: Accepted
This approach is efficient enough for most inputs and is a common interview solution.