You are given a string s and a dictionary of strings wordDict, add spaces in s to construct a sentence where each word is a valid dictionary word. Return all such possible sentences in any order.
Note that the same word in the dictionary may be reused multiple times in the segmentation.
Example 1:
Input: s ="neetcode", wordDict =["neet","code"]Output:["neet code"]
Example 2:
Input: s ="racecariscar", wordDict =["racecar","race","car","is"]Output:["racecar is car","race car is car"]
Example 3:
Input: s ="catsincars", wordDict =["cats","cat","sin","in","car"]Output:[]
Constraints:
1 <= s.length <= 20
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 10
s and wordDict[i] consist of only lowercase English letters.
All the strings of wordDict are unique.
Input is generated in a way that the length of the answer doesn't exceed 100,000.
Please upgrade to NeetCode Pro to view company tags.
Prerequisites
Before attempting this problem, you should be comfortable with:
Backtracking - Core technique for exploring all possible word segmentations by trying each valid prefix and recursing on the remainder
Dynamic Programming (Memoization) - Caching results for each starting index avoids recomputing the same suffix segmentations
Trie Data Structure - Optional optimization for efficient prefix matching when the dictionary has many words with common prefixes
Hash Sets - Converting the word dictionary to a set enables O(1) word lookup instead of O(m) list search
1. Backtracking
Intuition
We need to find all possible ways to segment the string into valid dictionary words. Starting from the beginning of the string, we try every possible prefix that exists in the dictionary. When we find a valid prefix, we recursively process the remaining substring. When we reach the end of the string, we have found a valid segmentation and add it to our result.
Algorithm
Convert the word dictionary to a set for O(1) lookups.
Use a recursive backtracking function starting at index 0.
At each position, try all substrings from the current index to the end.
If a substring exists in the dictionary, add it to the current path and recurse on the remaining string.
When the index reaches the end of the string, join the current path with spaces and add to results.
Backtrack by removing the last word from the path after each recursive call.
classSolution:defwordBreak(self, s:str, wordDict: List[str])-> List[str]:
wordDict =set(wordDict)defbacktrack(i):if i ==len(s):
res.append(" ".join(cur))returnfor j inrange(i,len(s)):
w = s[i:j +1]if w in wordDict:
cur.append(w)
backtrack(j +1)
cur.pop()
cur =[]
res =[]
backtrack(0)return res
classSolution{/**
* @param {string} s
* @param {string[]} wordDict
* @return {string[]}
*/wordBreak(s, wordDict){const wordSet =newSet(wordDict);const res =[];const cur =[];constbacktrack=(i)=>{if(i === s.length){
res.push(cur.join(' '));return;}for(let j = i; j < s.length; j++){const w = s.substring(i, j +1);if(wordSet.has(w)){
cur.push(w);backtrack(j +1);
cur.pop();}}};backtrack(0);return res;}}
publicclassSolution{publicList<string>WordBreak(string s,List<string> wordDict){HashSet<string> wordSet =newHashSet<string>(wordDict);List<string> res =newList<string>();List<string> cur =newList<string>();voidBacktrack(int i){if(i == s.Length){
res.Add(string.Join(" ", cur));return;}for(int j = i; j < s.Length; j++){string word = s.Substring(i, j - i +1);if(wordSet.Contains(word)){
cur.Add(word);Backtrack(j +1);
cur.RemoveAt(cur.Count -1);}}}Backtrack(0);return res;}}
funcwordBreak(s string, wordDict []string)[]string{
wordSet :=make(map[string]bool)for_, word :=range wordDict {
wordSet[word]=true}var res []stringvar cur []stringvar backtrack func(i int)
backtrack =func(i int){if i ==len(s){
res =append(res, strings.Join(cur," "))return}for j := i; j <len(s); j++{
w := s[i : j+1]if wordSet[w]{
cur =append(cur, w)backtrack(j +1)
cur = cur[:len(cur)-1]}}}backtrack(0)return res
}
class Solution {funwordBreak(s: String, wordDict: List<String>): List<String>{val wordSet = wordDict.toHashSet()val res = mutableListOf<String>()val cur = mutableListOf<String>()funbacktrack(i: Int){if(i == s.length){
res.add(cur.joinToString(" "))return}for(j in i until s.length){val w = s.substring(i, j +1)if(w in wordSet){
cur.add(w)backtrack(j +1)
cur.removeAt(cur.size -1)}}}backtrack(0)return res
}}
classSolution{funcwordBreak(_ s:String,_ wordDict:[String])->[String]{let wordSet =Set(wordDict)var res =[String]()var cur =[String]()let chars =Array(s)funcbacktrack(_ i:Int){if i == chars.count {
res.append(cur.joined(separator:" "))return}for j in i..<chars.count {let w =String(chars[i...j])if wordSet.contains(w){
cur.append(w)backtrack(j +1)
cur.removeLast()}}}backtrack(0)return res
}}
Time & Space Complexity
Time complexity: O(m+n∗2n)
Space complexity: O(m+2n)
Where n is the length of the string s and m is the sum of the lengths of the strings in the wordDict.
2. Backtracking + Trie
Intuition
Building a Trie from the dictionary words allows us to efficiently check prefixes while traversing the string. Instead of checking each substring against a set, we walk character by character through the Trie. This can provide early termination when no dictionary word starts with the current prefix, avoiding unnecessary substring operations.
Algorithm
Build a Trie by inserting all words from the dictionary.
Use backtracking starting at index 0 with an empty path.
At each position, traverse the Trie character by character from the current index.
When reaching a node marked as a word end, add that word to the path and recurse on the remaining string.
If a character is not found in the Trie, stop exploring that branch early.
When index reaches the end, join the path and add to results. Backtrack by removing the last word.
classTrieNode{constructor(){this.children =newMap();this.isWord =false;}}classTrie{constructor(){this.root =newTrieNode();}/**
* @param {string} word
* @return {void}
*/addWord(word){let curr =this.root;for(const c of word){if(!curr.children.has(c)){
curr.children.set(c,newTrieNode());}
curr = curr.children.get(c);}
curr.isWord =true;}}classSolution{/**
* @param {string} s
* @param {string[]} wordDict
* @return {string[]}
*/wordBreak(s, wordDict){const trie =newTrie();for(const word of wordDict){
trie.addWord(word);}const res =[];constbacktrack=(index, path)=>{if(index === s.length){
res.push(path.join(' '));return;}let node = trie.root;let word ='';for(let i = index; i < s.length; i++){const char = s[i];if(!node.children.has(char)){break;}
word += char;
node = node.children.get(char);if(node.isWord){
path.push(word);backtrack(i +1, path);
path.pop();}}};backtrack(0,[]);return res;}}
publicclassTrieNode{publicDictionary<char, TrieNode> children =newDictionary<char, TrieNode>();publicbool isWord =false;}publicclassTrie{publicTrieNode root =newTrieNode();publicvoidAddWord(string word){TrieNode curr = root;foreach(char c in word){if(!curr.children.ContainsKey(c)){
curr.children[c]=newTrieNode();}
curr = curr.children[c];}
curr.isWord =true;}}publicclassSolution{publicList<string>WordBreak(string s,List<string> wordDict){Trie trie =newTrie();foreach(string word in wordDict){
trie.AddWord(word);}List<string> res =newList<string>();voidBacktrack(int index,List<string> path){if(index == s.Length){
res.Add(string.Join(" ", path));return;}TrieNode node = trie.root;StringBuilder word =newStringBuilder();for(int i = index; i < s.Length; i++){char c = s[i];if(!node.children.ContainsKey(c))break;
word.Append(c);
node = node.children[c];if(node.isWord){
path.Add(word.ToString());Backtrack(i +1, path);
path.RemoveAt(path.Count -1);}}}Backtrack(0,newList<string>());return res;}}
Time & Space Complexity
Time complexity: O(m+n∗2n)
Space complexity: O(m+2n)
Where n is the length of the string s and m is the sum of the lengths of the strings in the wordDict.
3. Dynamic Programming (Top-Down)
Intuition
The pure backtracking approach may recompute results for the same suffix multiple times. By caching the list of all valid sentences that can be formed starting from each index, we avoid redundant computation. When we encounter a starting position we have already processed, we simply return the cached result.
Algorithm
Convert the dictionary to a set and create a cache dictionary.
Define a recursive function that returns all valid sentences from index i to end.
Base case: when i equals the string length, return a list containing an empty string.
If i is in the cache, return the cached result.
Try all substrings from i to end. For each valid dictionary word, get all sentences from the next position and prepend the current word.
classSolution:defwordBreak(self, s:str, wordDict: List[str])-> List[str]:
wordDict =set(wordDict)
cache ={}defbacktrack(i):if i ==len(s):return[""]if i in cache:return cache[i]
res =[]for j inrange(i,len(s)):
w = s[i:j +1]if w notin wordDict:continue
strings = backtrack(j +1)for substr in strings:
sentence = w
if substr:
sentence +=" "+ substr
res.append(sentence)
cache[i]= res
return res
return backtrack(0)
funcwordBreak(s string, wordDict []string)[]string{
wordSet :=make(map[string]bool)for_, word :=range wordDict {
wordSet[word]=true}
cache :=make(map[int][]string)var backtrack func(i int)[]string
backtrack =func(i int)[]string{if i ==len(s){return[]string{""}}if cached, ok := cache[i]; ok {return cached
}
res :=[]string{}for j := i; j <len(s); j++{
w := s[i : j+1]if!wordSet[w]{continue}
substrings :=backtrack(j +1)for_, substr :=range substrings {
sentence := w
if substr !=""{
sentence +=" "+ substr
}
res =append(res, sentence)}}
cache[i]= res
return res
}returnbacktrack(0)}
class Solution {funwordBreak(s: String, wordDict: List<String>): List<String>{val wordSet = wordDict.toHashSet()val cache = mutableMapOf<Int, List<String>>()funbacktrack(i: Int): List<String>{if(i == s.length)returnlistOf("")
cache[i]?.let{return it }val res = mutableListOf<String>()for(j in i until s.length){val w = s.substring(i, j +1)if(w !in wordSet)continueval substrings =backtrack(j +1)for(substr in substrings){val sentence =if(substr.isNotEmpty())"$w$substr"else w
res.add(sentence)}}
cache[i]= res
return res
}returnbacktrack(0)}}
classSolution{funcwordBreak(_ s:String,_ wordDict:[String])->[String]{let wordSet =Set(wordDict)var cache =[Int:[String]]()let chars =Array(s)funcbacktrack(_ i:Int)->[String]{if i == chars.count {return[""]}iflet cached = cache[i]{return cached
}var res =[String]()for j in i..<chars.count {let w =String(chars[i...j])if!wordSet.contains(w){continue}let substrings =backtrack(j +1)for substr in substrings {let sentence = substr.isEmpty ? w :"\(w)\(substr)"
res.append(sentence)}}
cache[i]= res
return res
}returnbacktrack(0)}}
Time & Space Complexity
Time complexity: O(m+n∗2n)
Space complexity: O(m+n∗2n)
Where n is the length of the string s and m is the sum of the lengths of the strings in the wordDict.
4. Dynamic Programming (Bottom-Up)
Intuition
Instead of recursing from the start, we can build the solution iteratively from the beginning. For each position i, we store all valid sentences that can be formed using characters from index 0 to i-1. We extend existing sentences by appending new words when a valid dictionary word ends at position i.
Algorithm
Create a DP array where dp[i] contains all valid sentences using the first i characters.
Initialize dp[0] with an empty string as the base case.
For each position i from 1 to n, check all possible last words ending at i.
If substring s[j:i] is in the dictionary and dp[j] is not empty, extend each sentence in dp[j] by appending the new word.
After processing all positions, dp[n] contains all valid segmentations of the entire string.
classSolution:defwordBreak(self, s:str, wordDict: List[str])-> List[str]:
wordSet =set(wordDict)
n =len(s)
dp =[[]for _ inrange(n +1)]
dp[0]=[""]for i inrange(1, n +1):for j inrange(i):if s[j:i]in wordSet:for sentence in dp[j]:
dp[i].append((sentence +" "+ s[j:i]).strip())return dp[n]
publicclassSolution{publicList<String>wordBreak(String s,List<String> wordDict){Set<String> wordSet =newHashSet<>(wordDict);int n = s.length();List<String>[] dp =newArrayList[n +1];for(int i =0; i <= n; i++){
dp[i]=newArrayList<>();}
dp[0].add("");for(int i =1; i <= n; i++){for(int j =0; j < i; j++){if(wordSet.contains(s.substring(j, i))){for(String sentence : dp[j]){
dp[i].add((sentence +" "+ s.substring(j, i)).trim());}}}}return dp[n];}}
classSolution{public:
vector<string>wordBreak(string s, vector<string>& wordDict){
unordered_set<string>wordSet(wordDict.begin(), wordDict.end());int n = s.size();
vector<vector<string>>dp(n +1);
dp[0]={""};for(int i =1; i <= n;++i){for(int j =0; j < i;++j){
string word = s.substr(j, i - j);if(wordSet.count(word)){for(const string& sentence : dp[j]){if(sentence.empty()){
dp[i].push_back(word);}else{
dp[i].push_back(sentence +" "+ word);}}}}}return dp[n];}};
classSolution{/**
* @param {string} s
* @param {string[]} wordDict
* @return {string[]}
*/wordBreak(s, wordDict){const wordSet =newSet(wordDict);const n = s.length;const dp = Array.from({length: n +1},()=>[]);
dp[0].push('');for(let i =1; i <= n; i++){for(let j =0; j < i; j++){if(wordSet.has(s.substring(j, i))){for(const sentence of dp[j]){
dp[i].push((sentence +' '+ s.substring(j, i)).trim());}}}}return dp[n];}}
publicclassSolution{publicList<string>WordBreak(string s,List<string> wordDictList){HashSet<string> wordSet =newHashSet<string>(wordDictList);int n = s.Length;List<string>[] dp =newList<string>[n +1];for(int i =0; i <= n; i++){
dp[i]=newList<string>();}
dp[0].Add("");for(int i =1; i <= n; i++){for(int j =0; j < i; j++){string word = s.Substring(j, i - j);if(wordSet.Contains(word)){foreach(string sentence in dp[j]){string space =string.IsNullOrEmpty(sentence)?"":" ";
dp[i].Add(sentence + space + word);}}}}return dp[n];}}
funcwordBreak(s string, wordDict []string)[]string{
wordSet :=make(map[string]bool)for_, word :=range wordDict {
wordSet[word]=true}
n :=len(s)
dp :=make([][]string, n+1)for i :=range dp {
dp[i]=[]string{}}
dp[0]=[]string{""}for i :=1; i <= n; i++{for j :=0; j < i; j++{
word := s[j:i]if wordSet[word]{for_, sentence :=range dp[j]{if sentence ==""{
dp[i]=append(dp[i], word)}else{
dp[i]=append(dp[i], sentence+" "+word)}}}}}return dp[n]}
class Solution {funwordBreak(s: String, wordDict: List<String>): List<String>{val wordSet = wordDict.toHashSet()val n = s.length
val dp =Array(n +1){ mutableListOf<String>()}
dp[0].add("")for(i in1..n){for(j in0 until i){val word = s.substring(j, i)if(word in wordSet){for(sentence in dp[j]){val space =if(sentence.isEmpty())""else" "
dp[i].add(sentence + space + word)}}}}return dp[n]}}
classSolution{funcwordBreak(_ s:String,_ wordDict:[String])->[String]{let wordSet =Set(wordDict)let chars =Array(s)let n = chars.count
var dp =[[String]](repeating:[String](), count: n +1)
dp[0]=[""]for i in1...n {for j in0..<i {let word =String(chars[j..<i])if wordSet.contains(word){for sentence in dp[j]{if sentence.isEmpty {
dp[i].append(word)}else{
dp[i].append("\(sentence)\(word)")}}}}}return dp[n]}}
Time & Space Complexity
Time complexity: O(m+n∗2n)
Space complexity: O(m+n∗2n)
Where n is the length of the string s and m is the sum of the lengths of the strings in the wordDict.
5. Dynamic Programming (Top-Down) Using Trie
Intuition
This combines the benefits of Trie-based prefix matching with memoization. The Trie provides efficient character-by-character matching and early termination, while the cache prevents recomputation of results for the same starting positions. This is particularly effective when the dictionary contains many words with common prefixes.
Algorithm
Build a Trie from all dictionary words.
Create a cache to store results for each starting index.
Define a recursive function that returns all sentences from index i.
At each index, traverse the Trie character by character while building words.
When a word boundary is found in the Trie, recursively get all suffixes and combine with the current word.
Cache and return the results. If a character is not in the Trie, terminate that branch early.
classTrieNode{constructor(){this.children =newMap();this.isWord =false;}}classTrie{constructor(){this.root =newTrieNode();}/**
* @param {string} word
* @return {void}
*/addWord(word){let curr =this.root;for(const c of word){if(!curr.children.has(c)){
curr.children.set(c,newTrieNode());}
curr = curr.children.get(c);}
curr.isWord =true;}}classSolution{/**
* @param {string} s
* @param {string[]} wordDict
* @return {string[]}
*/wordBreak(s, wordDict){const trie =newTrie();for(const word of wordDict){
trie.addWord(word);}const cache =newMap();constbacktrack=(index)=>{if(index === s.length){return[''];}if(cache.has(index)){return cache.get(index);}const res =[];let curr = trie.root;for(let i = index; i < s.length; i++){const char = s[i];if(!curr.children.has(char)){break;}
curr = curr.children.get(char);if(curr.isWord){for(const suffix ofbacktrack(i +1)){if(suffix){
res.push(s.slice(index, i +1)+' '+ suffix);}else{
res.push(s.slice(index, i +1));}}}}
cache.set(index, res);return res;};returnbacktrack(0);}}
publicclassTrieNode{publicDictionary<char, TrieNode> Children =newDictionary<char, TrieNode>();publicbool IsWord =false;}publicclassTrie{publicTrieNode Root =newTrieNode();publicvoidAddWord(string word){TrieNode curr = Root;foreach(char c in word){if(!curr.Children.ContainsKey(c)){
curr.Children[c]=newTrieNode();}
curr = curr.Children[c];}
curr.IsWord =true;}}publicclassSolution{privateDictionary<int, List<string>> cache =newDictionary<int, List<string>>();publicList<string>WordBreak(string s,List<string> wordDict){Trie trie =newTrie();foreach(string word in wordDict){
trie.AddWord(word);}returnBacktrack(0, s, trie.Root, trie);}privateList<string>Backtrack(int index,string s,TrieNode root,Trie trie){if(index == s.Length)returnnewList<string>{""};if(cache.ContainsKey(index))return cache[index];List<string> res =newList<string>();TrieNode curr = root;for(int i = index; i < s.Length; i++){char c = s[i];if(!curr.Children.ContainsKey(c))break;
curr = curr.Children[c];if(curr.IsWord){List<string> suffixes =Backtrack(i +1, s, root, trie);foreach(string suffix in suffixes){if(suffix ==""){
res.Add(s.Substring(index, i - index +1));}else{
res.Add(s.Substring(index, i - index +1)+" "+ suffix);}}}}
cache[index]= res;return res;}}
type TrieNode struct{
children map[rune]*TrieNode
isWord bool}funcnewTrieNode()*TrieNode {return&TrieNode{children:make(map[rune]*TrieNode)}}type Trie struct{
root *TrieNode
}funcnewTrie()*Trie {return&Trie{root:newTrieNode()}}func(t *Trie)addWord(word string){
curr := t.root
for_, c :=range word {if_, ok := curr.children[c];!ok {
curr.children[c]=newTrieNode()}
curr = curr.children[c]}
curr.isWord =true}funcwordBreak(s string, wordDict []string)[]string{
trie :=newTrie()for_, word :=range wordDict {
trie.addWord(word)}
cache :=make(map[int][]string)var backtrack func(index int)[]string
backtrack =func(index int)[]string{if index ==len(s){return[]string{""}}if cached, ok := cache[index]; ok {return cached
}
res :=[]string{}
curr := trie.root
for i := index; i <len(s); i++{
c :=rune(s[i])if_, ok := curr.children[c];!ok {break}
curr = curr.children[c]if curr.isWord {for_, suffix :=rangebacktrack(i +1){if suffix ==""{
res =append(res, s[index:i+1])}else{
res =append(res, s[index:i+1]+" "+suffix)}}}}
cache[index]= res
return res
}returnbacktrack(0)}
class TrieNode {val children = mutableMapOf<Char, TrieNode>()var isWord =false}class Trie {val root =TrieNode()funaddWord(word: String){var curr = root
for(c in word){
curr = curr.children.getOrPut(c){TrieNode()}}
curr.isWord =true}}class Solution {funwordBreak(s: String, wordDict: List<String>): List<String>{val trie =Trie()for(word in wordDict){
trie.addWord(word)}val cache = mutableMapOf<Int, List<String>>()funbacktrack(index: Int): List<String>{if(index == s.length)returnlistOf("")
cache[index]?.let{return it }val res = mutableListOf<String>()var curr = trie.root
for(i in index until s.length){val c = s[i]
curr = curr.children[c]?:breakif(curr.isWord){for(suffix inbacktrack(i +1)){if(suffix.isEmpty()){
res.add(s.substring(index, i +1))}else{
res.add(s.substring(index, i +1)+" "+ suffix)}}}}
cache[index]= res
return res
}returnbacktrack(0)}}
classTrieNode{var children =[Character:TrieNode]()var isWord =false}classTrie{let root =TrieNode()funcaddWord(_ word:String){var curr = root
for c in word {if curr.children[c]==nil{
curr.children[c]=TrieNode()}
curr = curr.children[c]!}
curr.isWord =true}}classSolution{funcwordBreak(_ s:String,_ wordDict:[String])->[String]{let trie =Trie()for word in wordDict {
trie.addWord(word)}let chars =Array(s)var cache =[Int:[String]]()funcbacktrack(_ index:Int)->[String]{if index == chars.count {return[""]}iflet cached = cache[index]{return cached
}var res =[String]()var curr = trie.root
for i in index..<chars.count {let c = chars[i]guardlet next = curr.children[c]else{break}
curr = next
if curr.isWord {for suffix inbacktrack(i +1){if suffix.isEmpty {
res.append(String(chars[index...i]))}else{
res.append(String(chars[index...i])+" "+ suffix)}}}}
cache[index]= res
return res
}returnbacktrack(0)}}
Time & Space Complexity
Time complexity: O(m+n∗2n)
Space complexity: O(m+n∗2n)
Where n is the length of the string s and m is the sum of the lengths of the strings in the wordDict.
Common Pitfalls
Not Handling the Base Case Correctly When Building Sentences
When recursion reaches the end of the string, returning an empty list [] instead of a list with an empty string [""] causes all valid sentences to be lost since there's nothing to append the last word to.
# Wrong: returns empty list, loses all sentencesif i ==len(s):return[]# Correct: returns list with empty string for concatenationif i ==len(s):return[""]
Forgetting to Add Space Between Words
When concatenating words to form sentences, forgetting to add spaces between words results in malformed output like "catsand" instead of "cats and".
# Wrong: no space between words
sentence = word + substr
# Correct: add space only if there's a suffix
sentence = word ifnot substr else word +" "+ substr
Using List Instead of Set for Dictionary Lookup
Checking if a substring exists in the word dictionary using a list results in O(m * t) lookup per check instead of O(t). This significantly slows down the solution.
# Wrong: O(m * t) lookup per checkif s[i:j+1]in wordDict:# wordDict is a list# Correct: O(t) lookup with hash set
wordSet =set(wordDict)if s[i:j+1]in wordSet:
Not Memoizing Results Leading to TLE
Without memoization, the same suffix is recomputed many times, leading to exponential time complexity even when not necessary. This causes Time Limit Exceeded on inputs with many overlapping subproblems.
# Wrong: recomputes same suffixes repeatedlydefbacktrack(i):if i ==len(s):return[""]
res =[]for j inrange(i,len(s)):# ... no caching# Correct: cache results for each starting indexdefbacktrack(i):if i in cache:return cache[i]# ... compute and store in cache[i]
Modifying Shared State Without Proper Backtracking
When using a shared list to build the current path, forgetting to pop the last word after recursion corrupts the path for other branches.
# Wrong: path keeps growing incorrectly
cur.append(word)
backtrack(j +1)# forgot to pop!# Correct: restore state after recursion
cur.append(word)
backtrack(j +1)
cur.pop()