🧠
Top-Down Memoization (Recursion + Cache)
💡 Memoization avoids recomputing results for the same substring, drastically improving efficiency while keeping the recursive structure.
Intuition
Cache the results of recursive calls so that each substring is solved once, then reused whenever needed.
Algorithm
- Initialize a cache (dictionary) to store results for substring indices.
- Recursively try every prefix; if prefix is in dictionary, recurse on suffix.
- Before recursion, check if result for suffix is cached; if yes, reuse it.
- Combine prefix with cached suffix sentences and store in cache.
- Return cached results for each substring.
💡 Memoization transforms exponential recursion into polynomial time by reusing results.
Recurrence:dp[i] = ∪ { word + ' ' + dp[i + len(word)] | word ∈ wordDict and s[i:].startsWith(word) } with memoization
from typing import List
def wordBreak(s: str, wordDict: List[str]) -> List[str]:
word_set = set(wordDict)
memo = {}
def backtrack(start):
if start in memo:
return memo[start]
if start == len(s):
return [""]
res = []
for end in range(start + 1, len(s) + 1):
prefix = s[start:end]
if prefix in word_set:
suffix_ways = backtrack(end)
for sentence in suffix_ways:
res.append(prefix + (" " + sentence if sentence else ""))
memo[start] = res
return res
return backtrack(0)
# Driver code
if __name__ == "__main__":
s = "catsanddog"
wordDict = ["cat", "cats", "and", "sand", "dog"]
print(wordBreak(s, wordDict))
Line Notes
memo = {}Initialize cache to store results for substring indices
if start in memo:Return cached result if available to avoid recomputation
memo[start] = resStore computed results in cache before returning
res.append(prefix + (" " + sentence if sentence else ""))Concatenate prefix with suffix sentences carefully to avoid trailing spaces
import java.util.*;
public class WordBreakII {
public List<String> wordBreak(String s, List<String> wordDict) {
Set<String> wordSet = new HashSet<>(wordDict);
Map<Integer, List<String>> memo = new HashMap<>();
return backtrack(s, 0, wordSet, memo);
}
private List<String> backtrack(String s, int start, Set<String> wordSet, Map<Integer, List<String>> memo) {
if (memo.containsKey(start)) {
return memo.get(start);
}
if (start == s.length()) {
return Arrays.asList("");
}
List<String> res = new ArrayList<>();
for (int end = start + 1; end <= s.length(); end++) {
String prefix = s.substring(start, end);
if (wordSet.contains(prefix)) {
List<String> suffixWays = backtrack(s, end, wordSet, memo);
for (String sentence : suffixWays) {
res.add(prefix + (sentence.isEmpty() ? "" : " " + sentence));
}
}
}
memo.put(start, res);
return res;
}
public static void main(String[] args) {
WordBreakII solver = new WordBreakII();
String s = "catsanddog";
List<String> wordDict = Arrays.asList("cat", "cats", "and", "sand", "dog");
System.out.println(solver.wordBreak(s, wordDict));
}
}
Line Notes
Map<Integer, List<String>> memo = new HashMap<>();Cache results for substring start indices
if (memo.containsKey(start)) {Return cached results if available
memo.put(start, res);Store computed results before returning
res.add(prefix + (sentence.isEmpty() ? "" : " " + sentence));Concatenate prefix and suffix sentences carefully
#include <iostream>
#include <vector>
#include <string>
#include <unordered_set>
#include <unordered_map>
using namespace std;
class Solution {
public:
vector<string> wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
unordered_map<int, vector<string>> memo;
return backtrack(s, 0, wordSet, memo);
}
private:
vector<string> backtrack(const string& s, int start, const unordered_set<string>& wordSet, unordered_map<int, vector<string>>& memo) {
if (memo.count(start)) {
return memo[start];
}
if (start == (int)s.size()) {
return {""};
}
vector<string> res;
for (int end = start + 1; end <= (int)s.size(); ++end) {
string prefix = s.substr(start, end - start);
if (wordSet.count(prefix)) {
vector<string> suffixWays = backtrack(s, end, wordSet, memo);
for (const string& sentence : suffixWays) {
res.push_back(prefix + (sentence.empty() ? "" : " " + sentence));
}
}
}
memo[start] = res;
return res;
}
};
int main() {
Solution sol;
string s = "catsanddog";
vector<string> wordDict = {"cat", "cats", "and", "sand", "dog"};
vector<string> result = sol.wordBreak(s, wordDict);
for (const string& sentence : result) {
cout << sentence << endl;
}
return 0;
}
Line Notes
unordered_map<int, vector<string>> memo;Cache results for substring start indices
if (memo.count(start)) {Return cached results if available
memo[start] = res;Store computed results before returning
res.push_back(prefix + (sentence.empty() ? "" : " " + sentence));Concatenate prefix and suffix sentences carefully
function wordBreak(s, wordDict) {
const wordSet = new Set(wordDict);
const memo = new Map();
function backtrack(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 prefix = s.substring(start, end);
if (wordSet.has(prefix)) {
const suffixWays = backtrack(end);
for (const sentence of suffixWays) {
res.push(sentence ? prefix + " " + sentence : prefix);
}
}
}
memo.set(start, res);
return res;
}
return backtrack(0);
}
// Test
console.log(wordBreak("catsanddog", ["cat", "cats", "and", "sand", "dog"]));
Line Notes
const memo = new Map();Cache results for substring start indices
if (memo.has(start)) return memo.get(start);Return cached results if available
memo.set(start, res);Store computed results before returning
res.push(sentence ? prefix + " " + sentence : prefix);Concatenate prefix and suffix sentences carefully
TimeO(n^3) in worst case due to memoization and string concatenations
SpaceO(n^3) for memo storage and recursion stack
Memoization ensures each substring is solved once, but concatenations and list merges add cubic overhead.
💡 For n=20, this approach is efficient enough to run comfortably within time limits.
Interview Verdict: Accepted / Efficient for interview coding
This is the recommended approach to implement in interviews for this problem.