Given a string s and a dictionary, return true if s
can be segmented into a space-free sequence of dictionary words (words
reusable).
Practice: LeetCode 139 — Word Break.
Pattern / family: String segmentation. These notes solve it with a
trie + BFS over start-positions; the mainstream solution is DP.
Examples. s="applepenapple", dict
{apple, pen} → true ("apple"+"pen"+"apple", apple reused).
s="catsandog", dict {cats, dog, sand, and, cat} → false:
every split leaves an "og" that isn't in the dictionary.
Approach — BFS over cut positions. Insert the
dictionary into a trie. BFS where each frontier item is a trie node being matched
from some start index. Advancing a character moves to a child; whenever a child is
isEndOfWord and that start index hasn't been used, you've completed a
word — enqueue a fresh root to begin the next word and mark the start
index visited. Succeed if you consume the whole string.
- ⭐ TRICK —
visited[] on start-index prevents exponential
blow-up. Many paths reach the same cut position; marking a start
index visited means you never re-explore segmentations from there. This is the
memoization that turns brute-force recursion into polynomial time.
- 💡 TIP — enqueue root on each word end. Finishing a word resets
matching to the trie root, because the next segment is a brand-new dictionary
lookup. That "restart from root" is the whole trick to chaining words.
- ⚠ PITFALL — this is fundamentally a DP problem. DP is a
natural fit here alongside the trie/BFS; note that the widely-taught optimal
is DP (
dp[i] = "s[0..i] segmentable"). Both are valid — know the DP
form for interviews.
- ⚠ PITFALL — same start index reused. Without the visited guard
you'd re-enqueue root for the same index repeatedly and loop.
Java — mainstream DP (recommended for interviews) [+].
O(n²) time (O(n³) if you count substring cost), O(n) space. Clearer to reason about
than the BFS.
boolean wordBreak(String s, List<String> wordDict) {
Set<String> dict = new HashSet<>(wordDict); // O(1) lookup
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true; // empty prefix is segmentable
for (int end = 1; end <= n; end++) {
for (int start = 0; start < end; start++) {
if (dp[start] && dict.contains(s.substring(start, end))) {
dp[end] = true; // s[0..end) segmentable
break;
}
}
}
return dp[n];
}
Java — trie + BFS.
boolean wordBreak(String s, List<String> wordDict) {
TrieNode trie = new TrieNode();
for (String w : wordDict) insert(w, trie);
return helper(s.toCharArray(), 0, trie);
}
boolean helper(char[] arr, int start, TrieNode root) {
Queue<TrieNode> q = new LinkedList<>();
q.add(root);
boolean[] visited = new boolean[arr.length];
while (!q.isEmpty() && start < arr.length) {
int size = q.size();
for (int i = 0; i < size; i++) {
TrieNode t = q.poll();
int idx = arr[start] - 'a';
if (t.children[idx] != null) {
q.add(t.children[idx]);
if (t.children[idx].isEndOfWord && !visited[start]) {
q.add(root); // word done → restart matching
visited[start] = true;
}
}
}
if (!q.isEmpty()) start++; // advance to next char
}
return start == arr.length && visited[arr.length - 1];
}
- Edge cases: empty
s → true; single char both
in/out of dict; heavy reuse (all same letter) stresses the visited guard.
- Follow-ups: Word Break II (return all segmentations — needs
backtracking, not just a boolean); why not DP here (it is the standard answer —
the trie version demonstrates trie flexibility).
Complexity: DP O(n²)/O(n); BFS bounded by n start-positions × trie
depth, ~O(n²) worst, plus O(dict) trie space. Why this matters:
"visited on position + BFS/DP over indices" recurs across segmentation and
reachability problems.