🧠
Trie + DFS with Memoization
💡 Using a Trie to store words allows prefix checks in O(L) time, improving efficiency over set lookups and enabling faster pruning.
Intuition
Build a Trie of all words, then for each word, use DFS to check if it can be formed by concatenating other words by traversing the Trie and memoizing results.
Algorithm
- Build a Trie containing all words.
- For each word, use DFS starting from index 0 to check if it can be formed by concatenation.
- At each DFS step, traverse the Trie nodes for prefixes; when a word end is found, recursively check the suffix.
- Use memoization to cache results for each start index to avoid repeated work.
💡 Trie traversal reduces prefix checks from O(L) substring lookups to O(1) per character, speeding up the search.
class TrieNode:
def __init__(self):
self.children = {}
self.is_word = False
class Solution:
def findAllConcatenatedWordsInADict(self, words):
root = TrieNode()
def insert(word):
node = root
for ch in word:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
node.is_word = True
for word in words:
if word:
insert(word)
memo = {}
def dfs(word, start):
if start == len(word):
return True
if start in memo:
return memo[start]
node = root
for i in range(start, len(word)):
ch = word[i]
if ch not in node.children:
break
node = node.children[ch]
if node.is_word:
if dfs(word, i + 1):
memo[start] = True
return True
memo[start] = False
return False
result = []
for word in words:
if not word:
continue
memo.clear()
# To avoid counting the word itself as concatenation, temporarily remove it
node = root
for ch in word:
node = node.children[ch]
node.is_word = False
if dfs(word, 0):
result.append(word)
node.is_word = True
return result
# Example usage:
if __name__ == '__main__':
sol = Solution()
words = ["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"]
print(sol.findAllConcatenatedWordsInADict(words))
Line Notes
class TrieNode:Define Trie node with children dictionary and is_word flag to mark word ends
def insert(word):Insert words into Trie for efficient prefix lookup
memo = {}Cache DFS results for start indices to avoid repeated work
def dfs(word, start):DFS to check if word can be formed by concatenation starting at index start
if start == len(word):Base case: reached end means successful concatenation
if start in memo:Return cached DFS result if available
for i in range(start, len(word))Traverse Trie nodes for prefixes starting at start index
if node.is_word:If prefix is a word, recursively check suffix starting at next index
node.is_word = FalseTemporarily disable current word to avoid counting itself as concatenation
node.is_word = TrueRestore word flag after DFS check
import java.util.*;
class TrieNode {
Map<Character, TrieNode> children = new HashMap<>();
boolean isWord = false;
}
public class Solution {
TrieNode root = new TrieNode();
public void insert(String word) {
TrieNode node = root;
for (char ch : word.toCharArray()) {
node.children.putIfAbsent(ch, new TrieNode());
node = node.children.get(ch);
}
node.isWord = true;
}
public boolean dfs(String word, int start, Map<Integer, Boolean> memo) {
if (start == word.length()) return true;
if (memo.containsKey(start)) return memo.get(start);
TrieNode node = root;
for (int i = start; i < word.length(); i++) {
char ch = word.charAt(i);
if (!node.children.containsKey(ch)) break;
node = node.children.get(ch);
if (node.isWord) {
if (dfs(word, i + 1, memo)) {
memo.put(start, true);
return true;
}
}
}
memo.put(start, false);
return false;
}
public List<String> findAllConcatenatedWordsInADict(String[] words) {
for (String word : words) {
if (!word.isEmpty()) insert(word);
}
List<String> result = new ArrayList<>();
for (String word : words) {
if (word.isEmpty()) continue;
TrieNode node = root;
for (char ch : word.toCharArray()) {
node = node.children.get(ch);
}
node.isWord = false; // temporarily disable
Map<Integer, Boolean> memo = new HashMap<>();
if (dfs(word, 0, memo)) {
result.add(word);
}
node.isWord = true; // restore
}
return result;
}
public static void main(String[] args) {
Solution sol = new Solution();
String[] words = {"cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"};
System.out.println(sol.findAllConcatenatedWordsInADict(words));
}
}
Line Notes
class TrieNode {Trie node with children map and isWord flag to mark word ends
public void insert(String word)Insert words into Trie for efficient prefix lookup
Map<Integer, Boolean> memoMemo cache for DFS start indices to avoid repeated work
if (start == word.length()) return true;Base case: full word matched means successful concatenation
for (int i = start; i < word.length(); i++)Traverse Trie nodes for prefixes starting at start index
if (node.isWord)If prefix is a word, recurse on suffix starting at next index
node.isWord = false;Temporarily disable current word to avoid self counting
node.isWord = true;Restore word flag after DFS
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <unordered_set>
using namespace std;
struct TrieNode {
unordered_map<char, TrieNode*> children;
bool isWord = false;
};
class Solution {
TrieNode* root = new TrieNode();
void insert(const string& word) {
TrieNode* node = root;
for (char ch : word) {
if (!node->children.count(ch))
node->children[ch] = new TrieNode();
node = node->children[ch];
}
node->isWord = true;
}
bool dfs(const string& word, int start, unordered_map<int, bool>& memo) {
if (start == (int)word.size()) return true;
if (memo.count(start)) return memo[start];
TrieNode* node = root;
for (int i = start; i < (int)word.size(); i++) {
char ch = word[i];
if (!node->children.count(ch)) break;
node = node->children[ch];
if (node->isWord) {
if (dfs(word, i + 1, memo)) {
memo[start] = true;
return true;
}
}
}
memo[start] = false;
return false;
}
public:
vector<string> findAllConcatenatedWordsInADict(vector<string>& words) {
for (const string& w : words) {
if (!w.empty()) insert(w);
}
vector<string> result;
for (const string& w : words) {
if (w.empty()) continue;
TrieNode* node = root;
for (char ch : w) {
node = node->children[ch];
}
node->isWord = false; // temporarily disable
unordered_map<int, bool> memo;
if (dfs(w, 0, memo)) result.push_back(w);
node->isWord = true; // restore
}
return result;
}
};
int main() {
Solution sol;
vector<string> words = {"cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"};
vector<string> res = sol.findAllConcatenatedWordsInADict(words);
for (auto& w : res) cout << w << "\n";
return 0;
}
Line Notes
struct TrieNode {Trie node with children map and isWord flag to mark word ends
void insert(const string& word)Insert words into Trie for efficient prefix lookup
unordered_map<int, bool>& memoMemo cache for DFS start indices to avoid repeated work
if (start == (int)word.size()) return true;Base case: full word matched means successful concatenation
for (int i = start; i < (int)word.size(); i++)Traverse Trie nodes for prefixes starting at start index
if (node->isWord)If prefix is a word, recurse on suffix starting at next index
node->isWord = false;Temporarily disable current word to avoid self counting
node->isWord = true;Restore word flag after DFS
class TrieNode {
constructor() {
this.children = new Map();
this.isWord = false;
}
}
var findAllConcatenatedWordsInADict = function(words) {
const root = new TrieNode();
function insert(word) {
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;
}
function dfs(word, start, memo) {
if (start === word.length) return true;
if (memo.has(start)) return memo.get(start);
let node = root;
for (let i = start; i < word.length; i++) {
const ch = word[i];
if (!node.children.has(ch)) break;
node = node.children.get(ch);
if (node.isWord) {
if (dfs(word, i + 1, memo)) {
memo.set(start, true);
return true;
}
}
}
memo.set(start, false);
return false;
}
for (const word of words) {
if (word) insert(word);
}
const result = [];
for (const word of words) {
if (!word) continue;
let node = root;
for (const ch of word) {
node = node.children.get(ch);
}
node.isWord = false; // temporarily disable
const memo = new Map();
if (dfs(word, 0, memo)) result.push(word);
node.isWord = true; // restore
}
return result;
};
// Example usage
const words = ["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"];
console.log(findAllConcatenatedWordsInADict(words));
Line Notes
class TrieNode {Trie node with children map and isWord flag to mark word ends
function insert(word)Insert words into Trie for efficient prefix lookup
function dfs(word, start, memo)DFS with memo to check if word can be formed by concatenation starting at index start
if (start === word.length) return true;Base case: full word matched means successful concatenation
for (let i = start; i < word.length; i++)Traverse Trie nodes for prefixes starting at start index
if (node.isWord)If prefix is a word, recurse on suffix starting at next index
node.isWord = false;Temporarily disable current word to avoid self counting
node.isWord = true;Restore word flag after DFS
TimeO(n * L^2) where n is number of words and L is max word length due to Trie traversal and memoization
SpaceO(n * L) for Trie nodes and memoization
Trie reduces prefix checks to O(L) per character, and memoization avoids repeated suffix checks, improving over set-based memoization.
💡 For max word length 30, this approach is efficient and scalable for large dictionaries.
Interview Verdict: Accepted / Optimal for interview coding
This approach shows mastery of Trie and DFS with memoization, a strong interview solution.