Bird
Raised Fist0
Interview PreptriesmediumAmazonFacebookGoogleBloomberg

Word Break (Trie Approach)

Choose your preparation mode4 modes available
🎯
Word Break (Trie Approach)
mediumTRIEAmazonFacebookGoogle

Imagine you have a dictionary of valid words and a long string typed without spaces. You want to check if the string can be segmented into a sequence of dictionary words, like auto-correcting a sentence typed without spaces.

💡 This problem involves segmenting a string into valid dictionary words. Beginners often struggle because the naive approach tries all segmentations, leading to exponential time. Using tries helps efficiently check prefixes, and memoization avoids repeated work, making the solution scalable.
📋
Problem Statement

Given a string s and a dictionary of strings wordDict, return true if s can be segmented into a space-separated sequence of one or more dictionary words. You may reuse dictionary words any number of times.

1 ≤ s.length ≤ 10^51 ≤ wordDict.length ≤ 10^41 ≤ wordDict[i].length ≤ 20s and wordDict[i] consist of only lowercase English letters
💡
Example
Input"s = \"leetcode\", wordDict = [\"leet\", \"code\"]"
Outputtrue

The string can be segmented as "leet code" which are both in the dictionary.

Input"s = \"applepenapple\", wordDict = [\"apple\", \"pen\"]"
Outputtrue

The string can be segmented as "apple pen apple".

Input"s = \"catsandog\", wordDict = [\"cats\", \"dog\", \"sand\", \"and\", \"cat\"]"
Outputfalse

No segmentation of the string into dictionary words is possible.

  • Empty string s → true (empty segmentation)
  • Dictionary contains single character words → segmentation possible if all chars in dict
  • String s with repeated characters not in dictionary → false
  • Dictionary with overlapping prefixes → ensure trie handles prefix branching correctly
⚠️
Common Mistakes
Not using memoization in recursion

Leads to exponential time and TLE on large inputs

✅ Add memo dictionary to cache results for start indices

Checking prefixes without efficient data structure

Repeated substring checks slow down solution

✅ Use trie to quickly check prefixes instead of substring lookups

Incorrect base case handling (not returning true for empty string)

Algorithm returns false even when segmentation is possible

✅ Return true when start index reaches string length

Not breaking early when dp[i] is true in iterative DP

Unnecessary computations increase runtime

✅ Break inner loop once dp[i] is set true

Building trie incorrectly (e.g., missing is_word flag)

Prefix checks fail to identify complete words, causing false negatives

✅ Mark end of each inserted word with is_word = True

🧠
Brute Force (Pure Recursion)
💡 This approach tries every possible prefix split recursively. It is simple and helps understand the problem but is inefficient due to repeated work and exponential time.

Intuition

Try every prefix of the string; if the prefix is in the dictionary, recursively check the remaining suffix. If any segmentation works, return true.

Algorithm

  1. Start from index 0 of the string.
  2. For each possible end index from start+1 to end of string:
  3. Check if substring from start to end is in the dictionary.
  4. If yes, recursively check if the rest of the string can be segmented.
  5. If any recursive call returns true, return true; else false.
💡 The recursion tree grows exponentially because each prefix choice leads to multiple suffix checks, making it hard to see the inefficiency at first.
</>
Code
class Solution:
    def wordBreak(self, s, wordDict):
        word_set = set(wordDict)
        def backtrack(start):
            if start == len(s):
                return True
            for end in range(start + 1, len(s) + 1):
                if s[start:end] in word_set and backtrack(end):
                    return True
            return False

# 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
word_set = set(wordDict)Convert list to set for O(1) membership checks to speed up prefix lookups
def backtrack(start):Define recursive function to check segmentation from index start
if start == len(s):Base case: reached end means successful segmentation of entire string
for end in range(start + 1, len(s) + 1):Try all possible prefixes starting at start index
if s[start:end] in word_set and backtrack(end):If prefix is in dictionary and suffix can be segmented, return True
return FalseNo valid segmentation found from this start index
import java.util.*;
public class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        Set<String> wordSet = new HashSet<>(wordDict);
        return backtrack(s, 0, wordSet);
    }
    private boolean backtrack(String s, int start, Set<String> wordSet) {
        if (start == s.length()) return true;
        for (int end = start + 1; end <= s.length(); end++) {
            if (wordSet.contains(s.substring(start, end)) && backtrack(s, end, wordSet)) {
                return true;
            }
        }
        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
Set<String> wordSet = new HashSet<>(wordDict);Use HashSet for fast lookup of dictionary words
private boolean backtrack(String s, int start, Set<String> wordSet)Recursive helper to check segmentation from start index
if (start == s.length()) return true;Base case: entire string segmented successfully
for (int end = start + 1; end <= s.length(); end++)Try all prefixes starting at start
if (wordSet.contains(s.substring(start, end)) && backtrack(s, end, wordSet))If prefix in dictionary and suffix valid, return true
return false;No valid segmentation found from this start index
#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        return backtrack(s, 0, wordSet);
    }
private:
    bool backtrack(const string& s, int start, unordered_set<string>& wordSet) {
        if (start == s.size()) return true;
        for (int end = start + 1; end <= s.size(); ++end) {
            if (wordSet.count(s.substr(start, end - start)) && backtrack(s, end, wordSet)) {
                return true;
            }
        }
        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_set<string> wordSet(wordDict.begin(), wordDict.end());Initialize hash set for O(1) lookups of dictionary words
bool backtrack(const string& s, int start, unordered_set<string>& wordSet)Recursive function to check segmentation from start index
if (start == s.size()) return true;Base case: full segmentation achieved
for (int end = start + 1; end <= s.size(); ++end)Try all prefixes starting at start
if (wordSet.count(s.substr(start, end - start)) && backtrack(s, end, wordSet))If prefix in dictionary and suffix valid, return true
return false;No valid segmentation found from this start index
var wordBreak = function(s, wordDict) {
    const wordSet = new Set(wordDict);
    function backtrack(start) {
        if (start === s.length) return true;
        for (let end = start + 1; end <= s.length; end++) {
            if (wordSet.has(s.substring(start, end)) && backtrack(end)) {
                return true;
            }
        }
        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
const wordSet = new Set(wordDict);Use Set for O(1) membership checks of dictionary words
function backtrack(start)Recursive helper to check segmentation from start index
if (start === s.length) return true;Base case: entire string segmented successfully
for (let end = start + 1; end <= s.length; end++)Try all prefixes starting at start
if (wordSet.has(s.substring(start, end)) && backtrack(end))If prefix in dictionary and suffix valid, return true
return false;No valid segmentation found from this start index
⏱
Complexity
TimeO(2^n) in worst case
SpaceO(n) recursion stack

At each index, we try all possible prefixes, leading to an exponential number of recursive calls.

💡 For n=20, this means potentially over a million calls, which is too slow for large inputs.
Interview Verdict: TLE

This approach is too slow for large inputs but is essential to understand the problem's nature and why optimization is needed.

🧠
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

  1. Build a trie from the word dictionary.
  2. Define a recursive function with memo to check segmentation from index start.
  3. From start, walk down the trie matching characters in s.
  4. If a trie node marks end of a word, recursively check the suffix starting after this word.
  5. Memoize results to avoid repeated work.
  6. 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.
</>
Code
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
⏱
Complexity
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.

🧠
Iterative DP with Trie Prefix Checks
💡 This approach uses bottom-up dynamic programming with a trie to check prefixes efficiently, avoiding recursion and stack overhead.

Intuition

Use a boolean DP array where dp[i] indicates if s[0:i] can be segmented. For each index i, check all prefixes ending at i using the trie. If any prefix is a word and dp at prefix start is true, set dp[i] true.

Algorithm

  1. Build a trie from the dictionary with words inserted in reverse order.
  2. Initialize dp array of size len(s)+1 with dp[0] = true (empty string).
  3. For each index i from 1 to len(s):
  4. Traverse backwards from i-1 down to 0, walking down the reversed trie matching s[j:i].
  5. If a trie node marks end of word and dp[j] is true, set dp[i] = true and break.
  6. Return dp[len(s)] as the answer.
💡 This bottom-up approach systematically builds solutions for all prefixes, using a reversed trie to efficiently find valid words ending at each position.
</>
Code
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 reversed(word):  # Insert reversed 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)
        n = len(s)
        dp = [False] * (n + 1)
        dp[0] = True

        for i in range(1, n + 1):
            node = root
            for j in range(i - 1, -1, -1):
                ch = s[j]
                if ch not in node.children:
                    break
                node = node.children[ch]
                if node.is_word and dp[j]:
                    dp[i] = True
                    break
        return dp[n]

# 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 reversed dictionary words
for ch in reversed(word): # Insert reversed wordInsert words in reverse order to enable backward traversal for suffix checks
dp = [False] * (n + 1)DP array to track segmentation up to each index
dp[0] = TrueEmpty string can always be segmented
for i in range(1, n + 1)Check segmentation for prefixes ending at i
for j in range(i - 1, -1, -1)Walk backwards to find valid word endings using reversed trie
if node.is_word and dp[j]If prefix is word and prefix start is segmentable, mark dp[i]
breakStop early once dp[i] is true to save computation
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 (int i = word.length() - 1; i >= 0; i--) { // Insert reversed
                char ch = word.charAt(i);
                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);
        int n = s.length();
        boolean[] dp = new boolean[n + 1];
        dp[0] = true;
        for (int i = 1; i <= n; i++) {
            TrieNode node = root;
            for (int j = i - 1; j >= 0; j--) {
                char ch = s.charAt(j);
                if (!node.children.containsKey(ch)) break;
                node = node.children.get(ch);
                if (node.isWord && dp[j]) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[n];
    }
    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
for (int i = word.length() - 1; i >= 0; i--) { // Insert reversedInsert words in reverse order to enable backward traversal for suffix checks
boolean[] dp = new boolean[n + 1];DP array to track segmentation status
dp[0] = true;Empty string is segmentable
for (int i = 1; i <= n; i++)Check all prefixes ending at i
for (int j = i - 1; j >= 0; j--)Walk backwards to find valid word endings using reversed trie
if (node.isWord && dp[j])If prefix is word and prefix start is segmentable, mark dp[i]
break;Stop early once dp[i] is true to save computation
#include <iostream>
#include <vector>
#include <unordered_map>
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 (int i = (int)word.size() - 1; i >= 0; --i) { // Insert reversed
                char ch = word[i];
                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);
        int n = s.size();
        vector<bool> dp(n + 1, false);
        dp[0] = true;
        for (int i = 1; i <= n; ++i) {
            TrieNode* node = root;
            for (int j = i - 1; j >= 0; --j) {
                char ch = s[j];
                if (!node->children.count(ch)) break;
                node = node->children[ch];
                if (node->isWord && dp[j]) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[n];
    }
};

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
for (int i = (int)word.size() - 1; i >= 0; --i) { // Insert reversedInsert words in reverse order to enable backward traversal for suffix checks
vector<bool> dp(n + 1, false);DP array to track segmentation status
dp[0] = true;Empty string is segmentable
for (int i = 1; i <= n; ++i)Check all prefixes ending at i
for (int j = i - 1; j >= 0; --j)Walk backwards to find valid word endings using reversed trie
if (node->isWord && dp[j])If prefix is word and prefix start is segmentable, mark dp[i]
break;Stop early once dp[i] is true to save computation
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 (let i = word.length - 1; i >= 0; i--) { // Insert reversed
            const ch = word[i];
            if (!node.children.has(ch)) node.children.set(ch, new TrieNode());
            node = node.children.get(ch);
        }
        node.isWord = true;
    }
    const n = s.length;
    const dp = new Array(n + 1).fill(false);
    dp[0] = true;
    for (let i = 1; i <= n; i++) {
        let node = root;
        for (let j = i - 1; j >= 0; j--) {
            const ch = s[j];
            if (!node.children.has(ch)) break;
            node = node.children.get(ch);
            if (node.isWord && dp[j]) {
                dp[i] = true;
                break;
            }
        }
    }
    return dp[n];
};

// 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
for (let i = word.length - 1; i >= 0; i--) { // Insert reversedInsert words in reverse order to enable backward traversal for suffix checks
const dp = new Array(n + 1).fill(false);DP array to track segmentation status
dp[0] = true;Empty string is segmentable
for (let i = 1; i <= n; i++)Check all prefixes ending at i
for (let j = i - 1; j >= 0; j--)Walk backwards to find valid word endings using reversed trie
if (node.isWord && dp[j])If prefix is word and prefix start is segmentable, mark dp[i]
break;Stop early once dp[i] is true to save computation
⏱
Complexity
TimeO(n * m) where n = length of s, m = max word length
SpaceO(n + total trie nodes) for dp and trie

For each position, we check up to m characters backwards using a reversed trie, so total O(n*m).

💡 This approach is efficient and avoids recursion stack overhead, suitable for large inputs.
Interview Verdict: Accepted

Iterative DP with reversed trie is a clean, efficient solution often preferred in interviews.

📊
All Approaches - One-Glance Tradeoffs
💡 Use the memoized recursion with trie or iterative DP with trie in 95% of interviews for best balance of clarity and efficiency.
ApproachTimeSpaceStack RiskReconstructUse In Interview
1. Brute ForceO(2^n)O(n) recursion stackYes (deep recursion)YesMention only - never code
2. Memoized Recursion with TrieO(n * m)O(n + trie size)No (memo reduces calls)YesGood to code if comfortable with recursion and tries
3. Iterative DP with TrieO(n * m)O(n + trie size)NoYesPreferred approach for clean iterative solution
💼
Interview Strategy
💡 Use this guide to understand the problem deeply, from brute force to optimized trie-based solutions. Practice explaining each approach clearly and test edge cases.

How to Present

Clarify problem constraints and examples.Explain brute force recursion and its inefficiency.Introduce trie and memoization to optimize prefix checks and avoid repeated work.Present iterative DP with trie as the optimal approach.Write clean code and test with sample and edge cases.

Time Allocation

Clarify: 3min → Approach: 5min → Code: 10min → Test: 5min. Total ~23min

What the Interviewer Tests

Interviewer tests your understanding of recursion, memoization, trie construction, and dynamic programming. They also check your ability to optimize and handle large inputs.

Common Follow-ups

  • How to generate all possible segmentations? → Use backtracking with memo to build all sentences.
  • What if dictionary is very large? → Use trie to reduce prefix checks and optimize memory.
💡 Follow-ups test your ability to extend the solution to harder variants and optimize for scale.
🔍
Pattern Recognition

When to Use

1) Asked to segment string into dictionary words 2) Need to check prefixes efficiently 3) Overlapping subproblems exist 4) Dictionary words reused multiple times

Signature Phrases

can be segmented into a sequence of dictionary wordsdictionary words can be reused any number of times

NOT This Pattern When

Problems that require substring matching without dictionary or no reuse of words

Similar Problems

Word Break II - generates all valid segmentations instead of boolean answerPalindrome Partitioning - segments string into palindromes instead of dictionary words

Practice

(1/5)
1. Consider the following buggy Trie insert method. Which line contains the subtle bug that causes search to fail for inserted words?
medium
A. Line where node = self.root is assigned
B. Line where node = node.children[ch] is assigned inside the loop
C. Line where isEnd flag should be set but is missing
D. Line where the if condition checks if ch not in node.children

Solution

  1. Step 1: Identify missing isEnd flag set

    After inserting all characters, isEnd must be set true to mark word end. This line is missing.
  2. Step 2: Understand impact on search

    Without isEnd=true, search returns false even if path exists, because node.isEnd remains false.
  3. Final Answer:

    Option C -> Option C
  4. Quick Check:

    Missing isEnd assignment causes search failure for inserted words [OK]
Hint: isEnd must be set after inserting entire word [OK]
Common Mistakes:
  • Forgetting to mark isEnd after insertion
2. The following code attempts to implement the optimal StreamChecker. Identify the single line that contains a subtle bug that can cause incorrect results or inefficiency.
class StreamChecker:
    def __init__(self, words):
        self.root = TrieNode()
        self.max_len = max(len(w) for w in words)
        self.stream = []
        for word in words:
            node = self.root
            for ch in word:  # Bug here: words not reversed
                if ch not in node.children:
                    node.children[ch] = TrieNode()
                node = node.children[ch]
            node.is_word = True

    def query(self, letter):
        self.stream.append(letter)
        if len(self.stream) > self.max_len:
            self.stream.pop(0)
        node = self.root
        for ch in reversed(self.stream):
            if ch not in node.children:
                return False
            node = node.children[ch]
            if node.is_word:
                return True
        return False
medium
A. Line: for ch in word:
B. Line: if len(self.stream) > self.max_len:
C. Line: for ch in reversed(self.stream):
D. Line: if node.is_word:

Solution

  1. Step 1: Identify how trie is built

    The code inserts words in forward order instead of reversed order, which breaks suffix matching logic.
  2. Step 2: Understand impact on query

    Query traverses reversed stream, expecting reversed trie. Since trie is built forward, traversal fails to find matches.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Reversing words during trie build is essential for suffix queries [OK]
Hint: Trie must be built with reversed words for suffix matching [OK]
Common Mistakes:
  • Not reversing words when building trie
  • Not limiting stream buffer
  • Returning false too early
3. Suppose the problem is modified so that words can be reused multiple times to form longer words (e.g., prefixes can be repeated). Which modification to the trie + BFS approach correctly handles this variant?
hard
A. No change needed; the existing BFS approach already handles reuse of words.
B. Modify BFS to enqueue child nodes even if child.word is None, allowing repeated prefixes.
C. Use DFS with memoization to explore repeated prefixes and avoid cycles.
D. Replace trie with a graph where edges represent possible concatenations and run BFS.

Solution

  1. Step 1: Understand reuse of words

    Allowing reuse means prefixes can be repeated multiple times, potentially causing cycles in traversal.
  2. Step 2: Why DFS with memoization is needed

    DFS with memoization prevents infinite loops by caching results for nodes, correctly handling repeated prefixes and cycles.
  3. Step 3: Why other options fail

    Existing BFS assumes no reuse; enqueuing nodes without word breaks prefix condition; graph BFS without cycle handling risks infinite loops.
  4. Final Answer:

    Option C -> Option C
  5. Quick Check:

    DFS + memoization handles repeated prefixes safely [OK]
Hint: Reuse requires cycle-safe DFS with memoization [OK]
Common Mistakes:
  • Assuming BFS handles reuse without changes
  • Enqueueing invalid nodes breaks prefix rules
  • Ignoring cycles in graph representation
4. Suppose the problem is modified so that words can be reused multiple times in pairs (i.e., pairs like (i, i) are allowed). Which modification to the Trie + DFS approach correctly handles this variant without producing duplicates or missing pairs?
hard
A. Modify add_word to store all occurrences of the same word and allow same index pairs
B. Keep the check that skips pairs with equal indices but add a separate pass to handle reuse pairs
C. Allow same index pairs by removing index checks but filter duplicates after collecting all pairs
D. Remove the check that skips pairs where indices are equal in search and palindrome_suffixes loops

Solution

  1. Step 1: Understand reuse allowance

    Allowing pairs (i, i) means removing index inequality checks but risks duplicates.
  2. Step 2: Modify search and add_word accordingly

    Removing index checks alone causes duplicates; filtering duplicates after collection ensures correctness.
  3. Step 3: Why other options fail

    Removing checks without filtering causes duplicates; separate pass is inefficient; storing all occurrences is unnecessary.
  4. Final Answer:

    Option C -> Option C
  5. Quick Check:

    Removing index checks + duplicate filtering handles reuse correctly [OK]
Hint: Allow reuse by removing index checks + filtering duplicates [OK]
Common Mistakes:
  • Removing index checks without filtering
  • Adding complex separate passes
  • Confusing occurrences with indices
5. Suppose the problem is modified so that each letter cell on the board can be reused multiple times within the same word (i.e., no visited marking needed). Which modification to the Trie + DFS algorithm correctly handles this variant without causing infinite loops or incorrect results?
hard
A. Remove visited marking and add a depth limit equal to the maximum word length to prevent infinite recursion.
B. Use BFS instead of DFS to avoid revisiting cells infinitely.
C. Keep visited marking but reset it after each DFS call to allow reuse.
D. Remove visited marking and keep DFS as is; no other changes needed.

Solution

  1. Step 1: Understand reuse allows cycles

    Without visited marking, DFS can revisit the same cell infinitely, causing infinite recursion.
  2. Step 2: Add depth limit to DFS

    Limiting recursion depth to the maximum word length prevents infinite loops while allowing reuse of cells.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Depth limit bounds recursion, enabling correct search with reuse [OK]
Hint: Depth limit prevents infinite recursion when reuse allowed [OK]
Common Mistakes:
  • Removing visited marking without depth limit causes infinite loops
  • Keeping visited marking contradicts reuse
  • Switching to BFS doesn't solve infinite recursion
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard