🧠
Memoized Recursion with Trie for Prefix Checking
💡 This approach uses a trie to efficiently check prefixes and memoization to avoid repeated work, drastically improving performance over brute force.
Intuition
Build a trie from the dictionary to quickly check if a prefix exists. Use recursion with memo to remember results for each start index, avoiding recomputation.
Algorithm
- Build a trie from the word dictionary.
- Define a recursive function with memo to check segmentation from index start.
- From start, walk down the trie matching characters in s.
- If a trie node marks end of a word, recursively check the suffix starting after this word.
- Memoize results to avoid repeated work.
- Return true if any segmentation succeeds, else false.
💡 Combining trie prefix checks with memoization avoids redundant substring checks and repeated recursion, making the solution efficient.
class TrieNode:
def __init__(self):
self.children = {}
self.is_word = False
class Solution:
def buildTrie(self, words):
root = TrieNode()
for word in words:
node = root
for ch in word:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
node.is_word = True
return root
def wordBreak(self, s, wordDict):
root = self.buildTrie(wordDict)
memo = {}
def backtrack(start):
if start == len(s):
return True
if start in memo:
return memo[start]
node = root
for end in range(start, len(s)):
ch = s[end]
if ch not in node.children:
break
node = node.children[ch]
if node.is_word and backtrack(end + 1):
memo[start] = True
return True
memo[start] = False
return False
return backtrack(0)
# Driver code
if __name__ == '__main__':
sol = Solution()
print(sol.wordBreak("leetcode", ["leet", "code"])) # True
print(sol.wordBreak("applepenapple", ["apple", "pen"])) # True
print(sol.wordBreak("catsandog", ["cats", "dog", "sand", "and", "cat"])) # False
Line Notes
class TrieNode:Define trie node with children and word end flag to represent dictionary words
def buildTrie(self, words):Build trie from dictionary words for efficient prefix checks
if start in memo:Return cached result to avoid recomputation and improve efficiency
for end in range(start, len(s))Walk down trie matching characters in s starting from index start
if node.is_word and backtrack(end + 1)If prefix is a word, recursively check suffix starting after this word
memo[start] = True/FalseCache result for start index to avoid repeated work
import java.util.*;
class TrieNode {
Map<Character, TrieNode> children = new HashMap<>();
boolean isWord = false;
}
public class Solution {
private TrieNode buildTrie(List<String> words) {
TrieNode root = new TrieNode();
for (String word : words) {
TrieNode node = root;
for (char ch : word.toCharArray()) {
node.children.putIfAbsent(ch, new TrieNode());
node = node.children.get(ch);
}
node.isWord = true;
}
return root;
}
public boolean wordBreak(String s, List<String> wordDict) {
TrieNode root = buildTrie(wordDict);
Map<Integer, Boolean> memo = new HashMap<>();
return backtrack(s, 0, root, memo);
}
private boolean backtrack(String s, int start, TrieNode root, Map<Integer, Boolean> memo) {
if (start == s.length()) return true;
if (memo.containsKey(start)) return memo.get(start);
TrieNode node = root;
for (int end = start; end < s.length(); end++) {
char ch = s.charAt(end);
if (!node.children.containsKey(ch)) break;
node = node.children.get(ch);
if (node.isWord && backtrack(s, end + 1, root, memo)) {
memo.put(start, true);
return true;
}
}
memo.put(start, false);
return false;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.wordBreak("leetcode", Arrays.asList("leet", "code"))); // true
System.out.println(sol.wordBreak("applepenapple", Arrays.asList("apple", "pen"))); // true
System.out.println(sol.wordBreak("catsandog", Arrays.asList("cats", "dog", "sand", "and", "cat"))); // false
}
}
Line Notes
Map<Character, TrieNode> children = new HashMap<>();Trie children map for each character to represent edges
TrieNode node = root;Start from trie root to insert or search prefixes
if (memo.containsKey(start)) return memo.get(start);Memoization to avoid repeated work and improve efficiency
for (int end = start; end < s.length(); end++)Traverse trie matching string characters from start index
if (node.isWord && backtrack(...))If prefix is word, recurse on suffix starting after this word
memo.put(start, true/false);Cache result for start index to avoid recomputation
#include <iostream>
#include <vector>
#include <unordered_map>
#include <unordered_set>
using namespace std;
struct TrieNode {
unordered_map<char, TrieNode*> children;
bool isWord = false;
};
class Solution {
public:
TrieNode* buildTrie(const vector<string>& words) {
TrieNode* root = new TrieNode();
for (const string& word : words) {
TrieNode* node = root;
for (char ch : word) {
if (!node->children.count(ch))
node->children[ch] = new TrieNode();
node = node->children[ch];
}
node->isWord = true;
}
return root;
}
bool wordBreak(string s, vector<string>& wordDict) {
TrieNode* root = buildTrie(wordDict);
unordered_map<int, bool> memo;
return backtrack(s, 0, root, memo);
}
private:
bool backtrack(const string& s, int start, TrieNode* root, unordered_map<int, bool>& memo) {
if (start == s.size()) return true;
if (memo.count(start)) return memo[start];
TrieNode* node = root;
for (int end = start; end < s.size(); ++end) {
char ch = s[end];
if (!node->children.count(ch)) break;
node = node->children[ch];
if (node->isWord && backtrack(s, end + 1, root, memo)) {
memo[start] = true;
return true;
}
}
memo[start] = false;
return false;
}
};
int main() {
Solution sol;
vector<string> dict1 = {"leet", "code"};
cout << boolalpha << sol.wordBreak("leetcode", dict1) << endl; // true
vector<string> dict2 = {"apple", "pen"};
cout << sol.wordBreak("applepenapple", dict2) << endl; // true
vector<string> dict3 = {"cats", "dog", "sand", "and", "cat"};
cout << sol.wordBreak("catsandog", dict3) << endl; // false
return 0;
}
Line Notes
unordered_map<char, TrieNode*> children;Trie children stored in hash map for fast access to next characters
TrieNode* node = root;Start from root to insert or search prefixes
if (memo.count(start)) return memo[start];Memoization to avoid recomputation and improve efficiency
for (int end = start; end < s.size(); ++end)Traverse trie matching characters in s from start index
if (node->isWord && backtrack(...))If prefix is word, recurse on suffix starting after this word
memo[start] = true/false;Cache result for start index to avoid repeated work
class TrieNode {
constructor() {
this.children = new Map();
this.isWord = false;
}
}
var wordBreak = function(s, wordDict) {
const root = new TrieNode();
for (const word of wordDict) {
let node = root;
for (const ch of word) {
if (!node.children.has(ch)) node.children.set(ch, new TrieNode());
node = node.children.get(ch);
}
node.isWord = true;
}
const memo = new Map();
function backtrack(start) {
if (start === s.length) return true;
if (memo.has(start)) return memo.get(start);
let node = root;
for (let end = start; end < s.length; end++) {
const ch = s[end];
if (!node.children.has(ch)) break;
node = node.children.get(ch);
if (node.isWord && backtrack(end + 1)) {
memo.set(start, true);
return true;
}
}
memo.set(start, false);
return false;
}
return backtrack(0);
};
// Test cases
console.log(wordBreak("leetcode", ["leet", "code"])); // true
console.log(wordBreak("applepenapple", ["apple", "pen"])); // true
console.log(wordBreak("catsandog", ["cats", "dog", "sand", "and", "cat"])); // false
Line Notes
this.children = new Map();Trie children stored in Map for fast lookup of next characters
if (memo.has(start)) return memo.get(start);Memoization to avoid repeated computations and improve efficiency
for (let end = start; end < s.length; end++)Traverse trie matching characters in s from start index
if (node.isWord && backtrack(end + 1))If prefix is word, recurse on suffix starting after this word
memo.set(start, true/false);Cache result for start index to avoid repeated work
return false;No valid segmentation found from this start index
TimeO(n * m) where n = length of s, m = max word length
SpaceO(n + total trie nodes) for memo and trie
Trie allows prefix checks in O(m), memo avoids repeated calls, so total calls are at most n, making overall complexity O(n*m).
💡 For n=100000 and max word length 20, this approach is efficient enough for interviews.
Interview Verdict: Accepted
This approach is efficient and practical, combining tries and memoization to solve the problem within constraints.