You are given an array of strings words(without duplicates), return all the concatenated words in the given list of words in any order.
A concatenated word is defined as a string that is comprised entirely of at least two shorter words (not necessarily distinct) in the given array.
Example 1:
Input: words =["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"]Output:["catsdogcats","dogcatsdog","ratcatdogcat"]
Explanation: "catsdogcats" can be concatenated by "cats", "dog" and "cats"; "dogcatsdog" can be concatenated by "dog", "cats" and "dog"; "ratcatdogcat" can be concatenated by "rat", "cat", "dog" and "cat".
Example 2:
Input: words =["cat","dog","catdog"]Output:["catdog"]
Constraints:
1 <= words.length <= 10,000
1 <= words[i].length <= 30
words[i] consists of only lowercase English letters.
Please upgrade to NeetCode Pro to view company tags.
Prerequisites
Before attempting this problem, you should be comfortable with:
Hash Sets - Using sets for O(1) lookups to check if a word exists in the dictionary
Recursion - Breaking down strings into prefix/suffix and recursively checking validity
Dynamic Programming - Using memoization to avoid recomputing results for the same substrings
String Manipulation - Splitting strings into substrings and concatenating words
1. Brute Force (Backtracking)
Intuition
We can generate all possible concatenations of words and check if any concatenation exists in our word set. By building concatenations through backtracking and checking membership, we find words that are formed by joining two or more shorter words.
Algorithm
Build a hash set from all words and find the maximum word length.
Use backtracking to generate all possible concatenations of words.
When a concatenation has more than one word and exists in the word set, add it to results and remove it from the set to avoid duplicates.
Prune branches where the current length exceeds the maximum word length.
classSolution:deffindAllConcatenatedWordsInADict(self, words: List[str])-> List[str]:
n =len(words)
res =[]
wordSet =set(words)
maxLen =0for w in words:
maxLen =max(maxLen,len(w))defdfs(concatWord, totLen):iflen(concatWord)>1:
word ="".join(concatWord)if word in wordSet:
res.append(word)
wordSet.remove(word)for i inrange(len(words)):if totLen +len(words[i])> maxLen:continue
concatWord.append(words[i])
dfs(concatWord, totLen +len(words[i]))
concatWord.pop()
dfs([],0)return res
classSolution{/**
* @param {string[]} words
* @return {string[]}
*/findAllConcatenatedWordsInADict(words){let n = words.length;let res =[];let wordSet =newSet(words);let maxLen =0;for(let w of words){
maxLen = Math.max(maxLen, w.length);}constdfs=(concatWord, totLen)=>{if(concatWord.length >1){let word = concatWord.join('');if(wordSet.has(word)){
res.push(word);
wordSet.delete(word);}}for(let i =0; i < words.length; i++){if(totLen + words[i].length > maxLen)continue;
concatWord.push(words[i]);dfs(concatWord, totLen + words[i].length);
concatWord.pop();}};dfs([],0);return res;}}
publicclassSolution{privateHashSet<string> wordSet;privateint maxLen;privateList<string> res;privatestring[] words;publicList<string>FindAllConcatenatedWordsInADict(string[] words){this.words = words;this.wordSet =newHashSet<string>(words);this.maxLen =0;this.res =newList<string>();foreach(var w in words){
maxLen = Math.Max(maxLen, w.Length);}Dfs(newList<string>(),0);return res;}privatevoidDfs(List<string> concatWord,int totLen){if(concatWord.Count >1){string word =string.Concat(concatWord);if(wordSet.Contains(word)){
res.Add(word);
wordSet.Remove(word);}}foreach(var word in words){if(totLen + word.Length > maxLen)continue;
concatWord.Add(word);Dfs(concatWord, totLen + word.Length);
concatWord.RemoveAt(concatWord.Count -1);}}}
funcfindAllConcatenatedWordsInADict(words []string)[]string{
wordSet :=make(map[string]bool)
maxLen :=0for_, w :=range words {
wordSet[w]=trueiflen(w)> maxLen {
maxLen =len(w)}}var res []stringvar dfs func(concatWord []string, totLen int)
dfs =func(concatWord []string, totLen int){iflen(concatWord)>1{
word := strings.Join(concatWord,"")if wordSet[word]{
res =append(res, word)delete(wordSet, word)}}for_, w :=range words {if totLen+len(w)> maxLen {continue}
concatWord =append(concatWord, w)dfs(concatWord, totLen+len(w))
concatWord = concatWord[:len(concatWord)-1]}}dfs([]string{},0)return res
}
class Solution {funfindAllConcatenatedWordsInADict(words: Array<String>): List<String>{val wordSet = words.toMutableSet()var maxLen =0for(w in words){
maxLen =maxOf(maxLen, w.length)}val res = mutableListOf<String>()fundfs(concatWord: MutableList<String>, totLen: Int){if(concatWord.size >1){val word = concatWord.joinToString("")if(word in wordSet){
res.add(word)
wordSet.remove(word)}}for(w in words){if(totLen + w.length > maxLen)continue
concatWord.add(w)dfs(concatWord, totLen + w.length)
concatWord.removeAt(concatWord.size -1)}}dfs(mutableListOf(),0)return res
}}
classSolution{funcfindAllConcatenatedWordsInADict(_ words:[String])->[String]{var wordSet =Set(words)var maxLen =0for w in words {
maxLen =max(maxLen, w.count)}var res =[String]()funcdfs(_ concatWord:inout[String],_ totLen:Int){if concatWord.count >1{let word = concatWord.joined()if wordSet.contains(word){
res.append(word)
wordSet.remove(word)}}for w in words {if totLen + w.count > maxLen {continue}
concatWord.append(w)dfs(&concatWord, totLen + w.count)
concatWord.removeLast()}}var concatWord =[String]()dfs(&concatWord,0)return res
}}
Time & Space Complexity
Time complexity: O(m∗nn)
Space complexity: O(m∗n)
Where n is the size of the string array words and m is the length of the longest word in the array.
2. Recursion
Intuition
For each word, we check if it can be split into parts where each part exists in the word set. We try every possible prefix; if a prefix is in the set, we recursively check if the remaining suffix can also be decomposed (or is itself in the set).
Algorithm
Build a hash set from all words for O(1) lookup.
For each word, define a recursive function that tries to split it.
At each position, check all possible prefixes. If a prefix exists in the set, check if the suffix also exists or can be recursively split.
If the entire word can be decomposed into at least two parts from the set, add it to results.
classSolution:deffindAllConcatenatedWordsInADict(self, words: List[str])-> List[str]:
wordSet =set(words)defdfs(word):for i inrange(1,len(word)):
prefix, suffix = word[:i], word[i:]if((prefix in wordSet and suffix in wordSet)or(prefix in wordSet and dfs(suffix))):returnTruereturnFalse
res =[]for w in words:if dfs(w):
res.append(w)return res
publicclassSolution{publicList<String>findAllConcatenatedWordsInADict(String[] words){Set<String> wordSet =newHashSet<>(Arrays.asList(words));List<String> res =newArrayList<>();for(String w : words){if(dfs(w, wordSet)){
res.add(w);}}return res;}privatebooleandfs(String word,Set<String> wordSet){for(int i =1; i < word.length(); i++){String prefix = word.substring(0, i);String suffix = word.substring(i);if((wordSet.contains(prefix)&& wordSet.contains(suffix))||(wordSet.contains(prefix)&&dfs(suffix, wordSet))){returntrue;}}returnfalse;}}
classSolution{public:
vector<string>findAllConcatenatedWordsInADict(vector<string>& words){
unordered_set<string>wordSet(words.begin(), words.end());
vector<string> res;for(const string& w : words){if(dfs(w, wordSet)){
res.push_back(w);}}return res;}private:booldfs(const string& word, unordered_set<string>& wordSet){for(int i =1; i < word.size(); i++){
string prefix = word.substr(0, i);
string suffix = word.substr(i);if((wordSet.count(prefix)&& wordSet.count(suffix))||(wordSet.count(prefix)&&dfs(suffix, wordSet))){returntrue;}}returnfalse;}};
classSolution{/**
* @param {string[]} words
* @return {string[]}
*/findAllConcatenatedWordsInADict(words){const wordSet =newSet(words);constdfs=(word)=>{for(let i =1; i < word.length; i++){const prefix = word.substring(0, i);const suffix = word.substring(i);if((wordSet.has(prefix)&& wordSet.has(suffix))||(wordSet.has(prefix)&&dfs(suffix))){returntrue;}}returnfalse;};const res =[];for(let w of words){if(dfs(w)){
res.push(w);}}return res;}}
publicclassSolution{privateHashSet<string> wordSet;publicList<string>FindAllConcatenatedWordsInADict(string[] words){
wordSet =newHashSet<string>(words);var res =newList<string>();foreach(var w in words){if(Dfs(w)){
res.Add(w);}}return res;}privateboolDfs(string word){for(int i =1; i < word.Length; i++){string prefix = word.Substring(0, i);string suffix = word.Substring(i);if((wordSet.Contains(prefix)&& wordSet.Contains(suffix))||(wordSet.Contains(prefix)&&Dfs(suffix))){returntrue;}}returnfalse;}}
funcfindAllConcatenatedWordsInADict(words []string)[]string{
wordSet :=make(map[string]bool)for_, w :=range words {
wordSet[w]=true}var dfs func(word string)bool
dfs =func(word string)bool{for i :=1; i <len(word); i++{
prefix, suffix := word[:i], word[i:]if(wordSet[prefix]&& wordSet[suffix])||(wordSet[prefix]&&dfs(suffix)){returntrue}}returnfalse}var res []stringfor_, w :=range words {ifdfs(w){
res =append(res, w)}}return res
}
class Solution {funfindAllConcatenatedWordsInADict(words: Array<String>): List<String>{val wordSet = words.toHashSet()fundfs(word: String): Boolean {for(i in1 until word.length){val prefix = word.substring(0, i)val suffix = word.substring(i)if((prefix in wordSet && suffix in wordSet)||(prefix in wordSet &&dfs(suffix))){returntrue}}returnfalse}val res = mutableListOf<String>()for(w in words){if(dfs(w)){
res.add(w)}}return res
}}
classSolution{funcfindAllConcatenatedWordsInADict(_ words:[String])->[String]{let wordSet =Set(words)funcdfs(_ word:String)->Bool{for i in1..<word.count {let prefixEnd = word.index(word.startIndex, offsetBy: i)letprefix=String(word[..<prefixEnd])let suffix =String(word[prefixEnd...])if(wordSet.contains(prefix)&& wordSet.contains(suffix))||(wordSet.contains(prefix)&&dfs(suffix)){returntrue}}returnfalse}var res =[String]()for w in words {ifdfs(w){
res.append(w)}}return res
}}
Time & Space Complexity
Time complexity: O(n∗m4)
Space complexity: O(n∗m)
Where n is the size of the string array words and m is the length of the longest word in the array.
3. Dynamic Programming (Top-Down)
Intuition
The recursive approach has overlapping subproblems since the same suffix may be checked multiple times. By memoizing results for each suffix, we avoid recomputing whether a substring can be decomposed.
Algorithm
Build a hash set from all words and create a memoization dictionary.
For each word, use a recursive function with memoization to check if it can be split.
Before computing, check if the result for the current word exists in memo.
Try all prefixes; if a prefix is in the word set and the suffix can be decomposed, cache and return true.
Cache false results as well to avoid recomputation. Add words that return true to the result.
classSolution:deffindAllConcatenatedWordsInADict(self, words: List[str])-> List[str]:
wordSet =set(words)
dp ={}defdfs(word):if word in dp:return dp[word]for i inrange(1,len(word)):
prefix, suffix = word[:i], word[i:]if((prefix in wordSet and suffix in wordSet)or(prefix in wordSet and dfs(suffix))):
dp[word]=TruereturnTrue
dp[word]=FalsereturnFalse
res =[]for w in words:if dfs(w):
res.append(w)return res
publicclassSolution{privateMap<String,Boolean> dp;publicList<String>findAllConcatenatedWordsInADict(String[] words){Set<String> wordSet =newHashSet<>(Arrays.asList(words));List<String> res =newArrayList<>();
dp =newHashMap<>();for(String w : words){if(dfs(w, wordSet)){
res.add(w);}}return res;}privatebooleandfs(String word,Set<String> wordSet){if(dp.containsKey(word)){return dp.get(word);}for(int i =1; i < word.length(); i++){String prefix = word.substring(0, i);String suffix = word.substring(i);if((wordSet.contains(prefix)&& wordSet.contains(suffix))||(wordSet.contains(prefix)&&dfs(suffix, wordSet))){
dp.put(word,true);returntrue;}}
dp.put(word,false);returnfalse;}}
classSolution{
unordered_map<string,bool> dp;public:
vector<string>findAllConcatenatedWordsInADict(vector<string>& words){
unordered_set<string>wordSet(words.begin(), words.end());
vector<string> res;for(const string& w : words){if(dfs(w, wordSet)){
res.push_back(w);}}return res;}private:booldfs(const string& word, unordered_set<string>& wordSet){if(dp.count(word)){return dp[word];}for(int i =1; i < word.size(); i++){
string prefix = word.substr(0, i);
string suffix = word.substr(i);if((wordSet.count(prefix)&& wordSet.count(suffix))||(wordSet.count(prefix)&&dfs(suffix, wordSet))){
dp[word]=true;returntrue;}}
dp[word]=false;returnfalse;}};
classSolution{/**
* @param {string[]} words
* @return {string[]}
*/findAllConcatenatedWordsInADict(words){const wordSet =newSet(words);const dp =newMap();constdfs=(word)=>{if(dp.has(word)){return dp.get(word);}for(let i =1; i < word.length; i++){const prefix = word.substring(0, i);const suffix = word.substring(i);if((wordSet.has(prefix)&& wordSet.has(suffix))||(wordSet.has(prefix)&&dfs(suffix))){
dp.set(word,true);returntrue;}}
dp.set(word,false);returnfalse;};const res =[];for(let w of words){if(dfs(w)){
res.push(w);}}return res;}}
publicclassSolution{privateHashSet<string> wordSet;privateDictionary<string,bool> dp;publicList<string>FindAllConcatenatedWordsInADict(string[] words){
wordSet =newHashSet<string>(words);
dp =newDictionary<string,bool>();var res =newList<string>();foreach(var w in words){if(Dfs(w)){
res.Add(w);}}return res;}privateboolDfs(string word){if(dp.TryGetValue(word,outbool val)){return val;}for(int i =1; i < word.Length; i++){string prefix = word.Substring(0, i);string suffix = word.Substring(i);if((wordSet.Contains(prefix)&& wordSet.Contains(suffix))||(wordSet.Contains(prefix)&&Dfs(suffix))){
dp[word]=true;returntrue;}}
dp[word]=false;returnfalse;}}
funcfindAllConcatenatedWordsInADict(words []string)[]string{
wordSet :=make(map[string]bool)for_, w :=range words {
wordSet[w]=true}
dp :=make(map[string]bool)
dpSet :=make(map[string]bool)var dfs func(word string)bool
dfs =func(word string)bool{if val, ok := dpSet[word]; ok {return val
}for i :=1; i <len(word); i++{
prefix, suffix := word[:i], word[i:]if(wordSet[prefix]&& wordSet[suffix])||(wordSet[prefix]&&dfs(suffix)){
dp[word]=true
dpSet[word]=truereturntrue}}
dp[word]=false
dpSet[word]=truereturnfalse}var res []stringfor_, w :=range words {ifdfs(w){
res =append(res, w)}}return res
}
class Solution {funfindAllConcatenatedWordsInADict(words: Array<String>): List<String>{val wordSet = words.toHashSet()val dp = mutableMapOf<String, Boolean>()fundfs(word: String): Boolean {if(word in dp){return dp[word]!!}for(i in1 until word.length){val prefix = word.substring(0, i)val suffix = word.substring(i)if((prefix in wordSet && suffix in wordSet)||(prefix in wordSet &&dfs(suffix))){
dp[word]=truereturntrue}}
dp[word]=falsereturnfalse}val res = mutableListOf<String>()for(w in words){if(dfs(w)){
res.add(w)}}return res
}}
classSolution{funcfindAllConcatenatedWordsInADict(_ words:[String])->[String]{let wordSet =Set(words)var dp =[String:Bool]()funcdfs(_ word:String)->Bool{iflet val = dp[word]{return val
}for i in1..<word.count {let prefixEnd = word.index(word.startIndex, offsetBy: i)letprefix=String(word[..<prefixEnd])let suffix =String(word[prefixEnd...])if(wordSet.contains(prefix)&& wordSet.contains(suffix))||(wordSet.contains(prefix)&&dfs(suffix)){
dp[word]=truereturntrue}}
dp[word]=falsereturnfalse}var res =[String]()for w in words {ifdfs(w){
res.append(w)}}return res
}}
Time & Space Complexity
Time complexity: O(n∗m3)
Space complexity: O(n∗m)
Where n is the size of the string array words and m is the length of the longest word in the array.
4. Dynamic Programming (Bottom-Up)
Intuition
For each word, we use a DP array where dp[i] indicates whether the substring from index 0 to i can be formed by concatenating words from the set. We build this array iteratively by checking all possible split points.
Algorithm
Build a hash set from all words.
For each word, create a boolean DP array of size m+1 with dp[0] = true.
For each position i from 1 to m, check all previous positions j. If dp[j] is true and the substring from j to i exists in the set, set dp[i] = true.
Skip the case where j = 0 and i = m to ensure the word is not just itself.
If dp[m] is true, the word is concatenated; add it to results.
classSolution:deffindAllConcatenatedWordsInADict(self, words: List[str])-> List[str]:
wordSet =set(words)
res =[]for word in words:
m =len(word)
dp =[False]*(m +1)
dp[0]=Truefor i inrange(1, m +1):for j inrange(i):if j ==0and i == m:continueif dp[j]and word[j:i]in wordSet:
dp[i]=Truebreakif dp[m]:
res.append(word)return res
publicclassSolution{publicList<String>findAllConcatenatedWordsInADict(String[] words){Set<String> wordSet =newHashSet<>(Arrays.asList(words));List<String> res =newArrayList<>();for(String word : words){int m = word.length();boolean[] dp =newboolean[m +1];
dp[0]=true;for(int i =1; i <= m; i++){for(int j =0; j < i; j++){if(j ==0&& i == m)continue;if(dp[j]&& wordSet.contains(word.substring(j, i))){
dp[i]=true;break;}}}if(dp[m]){
res.add(word);}}return res;}}
classSolution{public:
vector<string>findAllConcatenatedWordsInADict(vector<string>& words){
unordered_set<string>wordSet(words.begin(), words.end());
vector<string> res;for(string& word : words){int m = word.length();
vector<bool>dp(m +1,false);
dp[0]=true;for(int i =1; i <= m; i++){for(int j =0; j < i; j++){if(j ==0&& i == m)continue;if(dp[j]&& wordSet.count(word.substr(j, i - j))){
dp[i]=true;break;}}}if(dp[m]){
res.push_back(word);}}return res;}};
classSolution{/**
* @param {string[]} words
* @return {string[]}
*/findAllConcatenatedWordsInADict(words){const wordSet =newSet(words);const res =[];for(const word of words){const m = word.length;const dp =newArray(m +1).fill(false);
dp[0]=true;for(let i =1; i <= m; i++){for(let j =0; j < i; j++){if(j ===0&& i === m)continue;if(dp[j]&& wordSet.has(word.substring(j, i))){
dp[i]=true;break;}}}if(dp[m]){
res.push(word);}}return res;}}
publicclassSolution{publicList<string>FindAllConcatenatedWordsInADict(string[] words){var wordSet =newHashSet<string>(words);var res =newList<string>();foreach(var word in words){int m = word.Length;var dp =newbool[m +1];
dp[0]=true;for(int i =1; i <= m; i++){for(int j =0; j < i; j++){if(j ==0&& i == m)continue;if(dp[j]&& wordSet.Contains(word.Substring(j, i - j))){
dp[i]=true;break;}}}if(dp[m]){
res.Add(word);}}return res;}}
funcfindAllConcatenatedWordsInADict(words []string)[]string{
wordSet :=make(map[string]bool)for_, w :=range words {
wordSet[w]=true}var res []stringfor_, word :=range words {
m :=len(word)
dp :=make([]bool, m+1)
dp[0]=truefor i :=1; i <= m; i++{for j :=0; j < i; j++{if j ==0&& i == m {continue}if dp[j]&& wordSet[word[j:i]]{
dp[i]=truebreak}}}if dp[m]{
res =append(res, word)}}return res
}
class Solution {funfindAllConcatenatedWordsInADict(words: Array<String>): List<String>{val wordSet = words.toHashSet()val res = mutableListOf<String>()for(word in words){val m = word.length
val dp =BooleanArray(m +1)
dp[0]=truefor(i in1..m){for(j in0 until i){if(j ==0&& i == m)continueif(dp[j]&& word.substring(j, i)in wordSet){
dp[i]=truebreak}}}if(dp[m]){
res.add(word)}}return res
}}
classSolution{funcfindAllConcatenatedWordsInADict(_ words:[String])->[String]{let wordSet =Set(words)var res =[String]()for word in words {let m = word.count
var dp =[Bool](repeating:false, count: m +1)
dp[0]=truelet chars =Array(word)for i in1...m {for j in0..<i {if j ==0&& i == m {continue}if dp[j]&& wordSet.contains(String(chars[j..<i])){
dp[i]=truebreak}}}if dp[m]{
res.append(word)}}return res
}}
Time & Space Complexity
Time complexity: O(n∗m3)
Space complexity: O(n∗m)
Where n is the size of the string array words and m is the length of the longest word in the array.
Common Pitfalls
Counting a Word as Its Own Concatenation
A concatenated word must be formed by at least two other words. If you check whether the entire word exists in the set without excluding it, every word would trivially match itself.
# Wrong: allows word to match itselfif word[j:i]in wordSet:
dp[i]=True# Correct: skip the case where the entire word is checkedif j ==0and i == m:continue
Not Requiring Multiple Words
The problem requires concatenation of two or more words. Checking only that the word can be split into valid parts without ensuring at least two parts will incorrectly include single words that exist in the dictionary.
Missing Memoization Leading to TLE
Without memoization, the same suffix may be checked exponentially many times. For long words with many valid prefixes, this causes timeout.
# Wrong: no cachingdefdfs(word):for i inrange(1,len(word)):if prefix in wordSet and dfs(suffix):returnTrue# Correct: cache resultsif word in dp:return dp[word]
Incorrect Substring Indices
Off-by-one errors when splitting the word into prefix and suffix are common. The prefix should be word[0:i] and suffix should be word[i:], where i ranges from 1 to len(word)-1.
Forgetting Empty String Edge Case
If the word list contains an empty string, it could match infinitely. Most solutions implicitly handle this since the loop starts at i=1, but explicitly filtering empty strings from the word set is safer.