Lesson 132 of 164
Word Formation From a Dictionary Using a Trie
Given a dictionary stored in a trie and a target word, return true if the target can be formed by joining exactly two dictionary words.
Example
dictionary: ["the", "hello", "there", "answer", "any", "by", "world", "their", "abc"]
"helloworld" true hello + world
"thereby" true there + by ("the" + "reby" fails, so keep looking)
"thebye" falseBrute force: search both halves at every split
For each split position i, search the trie for word.substring(0, i) and for word.substring(i). A trie node is an object with an array of 26 child references, where null means "no such child".
public class Main {
static class TrieNode {
TrieNode[] children = new TrieNode[26];
boolean isEnd;
}
static void insert(TrieNode root, String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
if (node.children[idx] == null) node.children[idx] = new TrieNode();
node = node.children[idx];
}
node.isEnd = true;
}
static boolean search(TrieNode root, String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
node = node.children[word.charAt(i) - 'a'];
if (node == null) return false;
}
return node.isEnd;
}
static boolean canForm(TrieNode root, String word) {
for (int i = 1; i < word.length(); i++) {
if (search(root, word.substring(0, i)) && search(root, word.substring(i))) {
return true;
}
}
return false;
}
public static void main(String[] args) {
TrieNode root = new TrieNode();
for (String w : new String[] {"the", "hello", "there", "answer", "any", "by", "world", "their", "abc"}) {
insert(root, w);
}
System.out.println(canForm(root, "helloworld") + " " + canForm(root, "thereby") + " " + canForm(root, "thebye"));
}
}true true falseTime: O(L²): L - 1 split positions, and each one searches up to L characters (and substring copies up to L characters). Space: O(L) for the copied substrings.
This repeats work: searching word.substring(0, 3), then word.substring(0, 4), then word.substring(0, 5) walks the same first characters again each time.
Optimal: walk the prefix once
Walk the target down the trie one character at a time, from the root. After each step, the current node represents the prefix word.substring(0, i + 1):
- If the path breaks (no child for the next character), no longer prefix can be a dictionary word, so stop and return
false. - If the current node has
isEndset, the prefix is a dictionary word. Check whether the rest,word.substring(i + 1), is a word too.
The walk visits each character of the target once, and only tries the second-half search at positions where the first half really is a word.
Walkthrough with "thereby":
| position | prefix | node exists? | is a word? | check the rest |
|---|---|---|---|---|
| 0 | t | yes | no | |
| 1 | th | yes | no | |
| 2 | the | yes | yes | "reby" → not a word, continue |
| 3 | ther | yes | no | |
| 4 | there | yes | yes | "by" → word → return true |
public class Main {
static class TrieNode {
TrieNode[] children = new TrieNode[26];
boolean isEnd;
}
static void insert(TrieNode root, String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
if (node.children[idx] == null) node.children[idx] = new TrieNode();
node = node.children[idx];
}
node.isEnd = true;
}
static boolean search(TrieNode root, String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
node = node.children[word.charAt(i) - 'a'];
if (node == null) return false;
}
return node.isEnd;
}
static boolean canForm(TrieNode root, String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
node = node.children[word.charAt(i) - 'a'];
if (node == null) return false; // no dictionary word starts with word[0..i]
int split = i + 1;
if (node.isEnd && split < word.length() && search(root, word.substring(split))) {
return true;
}
}
return false;
}
public static void main(String[] args) {
TrieNode root = new TrieNode();
for (String w : new String[] {"the", "hello", "there", "answer", "any", "by", "world", "their", "abc"}) {
insert(root, w);
}
System.out.println(canForm(root, "helloworld") + " " + canForm(root, "thereby") + " " + canForm(root, "thebye"));
System.out.println(canForm(root, "byby")); // the same word twice
}
}true true false
trueTime: O(L + p · L), where p is the number of dictionary words that are prefixes of the target. In the worst case that's still O(L²), but usually p is small, and the walk stops early when no dictionary word matches the start. Space: O(L) for the substring copy made at each split attempt.
split < word.length() makes sure the second word isn't empty. Without it, a target that is itself a single dictionary word would count as "word + nothing".
Related: any number of words
Word Break (LeetCode 139) allows splitting into any number of dictionary words. The same trie walk becomes a dynamic program: can[i] means word.substring(0, i) can be built. From each i with can[i] true, walk the trie from position i and set can[j] for every j where the walk passes an end node.
public class Main {
static class TrieNode {
TrieNode[] children = new TrieNode[26];
boolean isEnd;
}
static void insert(TrieNode root, String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
if (node.children[idx] == null) node.children[idx] = new TrieNode();
node = node.children[idx];
}
node.isEnd = true;
}
static boolean wordBreak(TrieNode root, String word) {
int n = word.length();
boolean[] can = new boolean[n + 1];
can[0] = true;
for (int start = 0; start < n; start++) {
if (!can[start]) continue;
TrieNode node = root;
for (int j = start; j < n; j++) {
node = node.children[word.charAt(j) - 'a'];
if (node == null) break;
if (node.isEnd) can[j + 1] = true;
}
}
return can[n];
}
public static void main(String[] args) {
TrieNode root = new TrieNode();
for (String w : new String[] {"apple", "pen", "applepen", "pine", "pineapple", "cats", "dog", "sand", "and", "cat"}) {
insert(root, w);
}
System.out.println(wordBreak(root, "pineapplepenapple") + " " + wordBreak(root, "catsandog"));
}
}true falseTime: O(L²) in the worst case. Space: O(L).
Word Break covers this problem in full, with a HashSet instead of a trie.
