Trie · 3 of 6
Word Break Problem (Trie solution)
Decide whether a string with no spaces can be cut into dictionary words, by walking a trie of the dictionary from every spot where a word could start.
- Simple way
- time O(2^n)
room O(n) - Clever way
- time O(n * K + D)
room O(n + 26 * D)
The problem, simply
You get a string s with no spaces, like catsanddog, and a dictionary of words. Can you put spaces into s so that every piece is a dictionary word? Words may be used many times. Say true or false.
Below, n is the length of s, K is the length of the longest dictionary word, and D is the total number of letters in the dictionary.
A story to remember it
Picture it
The trie for the dictionary cat, cats, and, sand and dog, next to the string. cat ends in the middle of the path for cats.
Examples
A normal case
- In
s = 'catsanddog', dict = [cat, cats, and, sand, dog]- Out
true
cat sand dog works, and so does cats and dog.
Reusing a word
- In
s = 'applepenapple', dict = [apple, pen]- Out
true
apple pen apple. A word may be used again.
Almost
- In
s = 'catsandog', dict = [cats, dog, sand, and, cat]- Out
false
cats and leaves og, and cat sand leaves og. No word covers the end.
A trap for plain recursion
- In
s = 'aaaaaaaaaaaaaaab', dict = [a, aa, aaa]- Out
false
There are thousands of ways to cut the a's, and none of them can handle the b. Plain recursion tries them all. The dp way checks each start once.
Empty string
- In
s = '', dict = [a]- Out
true
Zero letters need zero words.
Way 1: the simple way
Try every first word. If the start of s is a dictionary word, cut it off and solve the rest the same way.
- If nothing is left, the answer is
true. - For every end position, if
s[start..end)is in the dictionary and the rest can be split, the answer istrue. - If no first word works, the answer is
false.
import java.util.*;
class Solution {
public static boolean wordBreak(String s, List<String> dict) {
return canSplit(s, 0, new HashSet<>(dict));
}
private static boolean canSplit(String s, int start, Set<String> words) {
if (start == s.length()) return true; // nothing left to cut
for (int end = start + 1; end <= s.length(); end++) {
if (words.contains(s.substring(start, end)) // first piece is a word
&& canSplit(s, end, words)) { // and the rest works out
return true;
}
}
return false;
}
}How long it takes: the same rest of the string gets solved again and again. With many short words this tries close to every way of cutting, O(2^n). Extra room: the call stack, O(n).
Way 2: the clever way
Remember answers in a table, and use a trie to find all words that start at a spot in one walk.
- Put every dictionary word in a trie.
- Make
dpof sizen + 1.dp[k]istruewhen the firstkletters can be cut into words. Setdp[0] = true. - For each
iwithdp[i]true, walk the trie withs[i], s[i+1], .... Each time you reach a word end at letterj, setdp[j + 1] = true. Stop when the trie has no branch for the next letter. - The answer is
dp[n].
Walk the trie from every marked stone
01 / 14
dp[k] answers: can the first k letters be cut into dictionary words? dp[0] is true, because zero letters need zero words. Every true dp[i] is a spot where a new word may start.
- dp[0]
- true
- looking now
- done
- answer
- rejected
- path
- just added
- ignored
import java.util.*;
class Solution {
private static class Node {
Node[] next = new Node[26];
boolean end;
}
public static boolean wordBreak(String s, List<String> dict) {
Node root = new Node();
for (String w : dict) { // build the trie
Node cur = root;
for (char ch : w.toCharArray()) {
int k = ch - 'a';
if (cur.next[k] == null) cur.next[k] = new Node();
cur = cur.next[k];
}
cur.end = true;
}
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true; // zero letters need zero words
for (int i = 0; i < n; i++) {
if (!dp[i]) continue; // no word may start here
Node cur = root;
for (int j = i; j < n; j++) {
cur = cur.next[s.charAt(j) - 'a'];
if (cur == null) break; // no word starts with s[i..j]
if (cur.end) dp[j + 1] = true; // s[i..j] is a word
}
}
return dp[n];
}
}How long it takes: building the trie is O(D). From each of the n starts, the walk stops after at most K letters, because no word is longer. So O(n * K + D). Extra room: the dp table and the trie, O(n + 26 * D).
Watch out for
- Forgetting
dp[0] = true. Without it nothing is ever marked and every answer isfalse. - Stopping at the first word found from a start.
catandcatsboth start at 0, and both matter. Keep walking until the trie runs out. - The answer lives in
dp[n], notdp[n - 1]. The table is one longer than the string.
What interviewers ask next
- Print every way to cut the string? Keep, for each end, the list of starts that reach it, then build the sentences backward with backtracking. The output itself can be huge.
- Can you do it with a hash set instead of a trie? Yes. For each reachable
i, checks.substring(i, j)forjup toi + K. Each check costsO(K)for hashing, so the trie is a bit faster and stops early. - Memoized recursion instead of the table? Same idea from the other side: remember for each start whether the rest can be split.
O(n * K)with the trie. - Fewest words needed? Turn
dpinto numbers and keep the smallest count, or run a breadth first search over start positions. - What if the dictionary is huge and
sis short? Then the trie still costs onlyO(D)once, and each query isO(n * K).
Remember this
Grown-up words: the stones are positions in the string, the chalk marks are the 1D DP table, the word tree is a trie of the dictionary, and the jumps that spell words are valid segments.