Concatenated Words
You are given a list of words, all different from one another. Return every word that is a concatenated word : a word made entirely by gluing together two or more shorter words that are also in the same list. For example, if the list contains "cat", "dog" and "catdog", then "catdog" is a concatenated word because it is exactly "cat" followed by "dog". Put every word into a trie. Then, for each word, walk its letters down the trie from the start. Every time you pass an end-of-word marker you have found one possible first piece, so you go back to the root and try to split the rest of the word the same way. That is a recursive search over split points . The word only counts if the pieces run out exactly at the end AND at least two pieces were used — otherwise every word would trivially match itself. Remembering which start positions already failed stops the search from redoing the same work over and over on long words.
Constraints
- 1 ≤ words.length ≤ 104
- 1 ≤ wordsi.length ≤ 30
- wordsi consists of lowercase English letters only
- All the words are different from one another
Example
words = ["cat", "cats", "catsdog", "dog"]["catsdog"]Explanation "catsdog" splits into "cats" and "dog", both of which are in the list, so it is returned. "cat", "cats" and "dog" are each only one piece long, so they do not count.
In plain terms
- Concatenation
- Gluing strings together end to end. "cat" concatenated with "dog" is "catdog".
- Split point
- A position where you cut the word into a finished piece and a remaining part still to be checked.
- Memoisation
- Writing down answers you already worked out so you never compute them twice. Here, the start positions that were already shown to fail.
- End-of-word marker
- A true/false flag on a trie node meaning "one of the stored words finishes right here".
Splitting "catsdog" using a trie of cat, cats and dog
The words cat, cats and dog are stored in the trie; now test "catsdog".
What happens in this step
words = ["cat", "cats", "catsdog", "dog"] testing word = 'catsdog' canSplit(word, start = 0, pieces = 0) The word itself is also in the trie, but matching the whole thing gives only one piece.
Steps to visualize
- The row of cells is the word being tested, one cell per letter.
- The value shows what the walk found at that letter: "node" means the path continues, "word!" means a piece can end there, "none" means the trie ran out.
- When a piece ends, the search jumps back to the root and keeps going from the next letter.
- A dead end simply backs up and tries the next possible split point instead.
Walk through the code
Same walkthrough, now with the code. Press Next to move one step and watch which lines run.
The words cat, cats and dog are stored in the trie; now test "catsdog".
What happens in this step
words = ["cat", "cats", "catsdog", "dog"] testing word = 'catsdog' canSplit(word, start = 0, pieces = 0) The word itself is also in the trie, but matching the whole thing gives only one piece.
Solution
function findAllConcatenatedWordsInADict(words) {
class TrieNode {
constructor() {
this.children = {};
this.isEnd = false;
}
}
const root = new TrieNode();
for (const word of words) {
if (word.length === 0) {
continue;
}
let node = root;
for (const ch of word) {
if (!node.children[ch]) {
node.children[ch] = new TrieNode();
}
node = node.children[ch];
}
node.isEnd = true;
}
const canSplit = (word, start, pieces, seen) => {
if (start === word.length) {
return pieces >= 2;
}
if (seen.has(start)) {
return false;
}
let node = root;
for (let i = start; i < word.length; i++) {
node = node.children[word[i]];
if (!node) {
break;
}
if (node.isEnd && canSplit(word, i + 1, pieces + 1, seen)) {
return true;
}
}
seen.add(start);
return false;
};
const answer = [];
for (const word of words) {
if (word.length > 0 && canSplit(word, 0, 0, new Set())) {
answer.push(word);
}
}
return answer;
}- Time
- O(n * L^2) where n is the number of words and L the longest word length
- Space
- O(total letters across all words)
Test cases
| Input | Expected | Covers |
|---|---|---|
words = ["cat", "cats", "catsdog", "dog"] | ["catsdog"] | example from the docstring |
words = ["cat", "cats", "catsdogcats", "dog", "dogcatsdog", "hippopotamuses", "rat", "ratcatdogcat"] | ["catsdogcats", "dogcatsdog", "ratcatdogcat"] | several concatenated words mixed in with words that are not |
words = ["abc", "def"] | [] | no word can be built from the others |
words = ["cat"] | [] | a word never counts as a concatenation of just itself |
words = ["a", "aa", "aaa"] | ["aa", "aaa"] | the same short word reused as several pieces |
words = ["", "a", "ab", "b"] | ["ab"] | an empty word must be ignored, not treated as a free piece |