139. Word Break - Explanation
Description
Given a string s and a dictionary of strings wordDict, return true if s can be segmented into a space-separated sequence of dictionary words.
You are allowed to reuse words in the dictionary an unlimited number of times. You may assume all dictionary words are unique.
Example 1:
Input: s = "neetcode", wordDict = ["neet","code"]
Output: trueExplanation: Return true because "neetcode" can be split into "neet" and "code".
Example 2:
Input: s = "applepenapple", wordDict = ["apple","pen","ape"]
Output: trueExplanation: Return true because "applepenapple" can be split into "apple", "pen" and "apple". Notice that we can reuse words and also not use all the words.
Example 3:
Input: s = "catsincars", wordDict = ["cats","cat","sin","in","car"]
Output: falseConstraints:
1 <= s.length <= 2001 <= wordDict.length <= 1001 <= wordDict[i].length <= 20sandwordDict[i]consist of only lowercase English letters.
Topics
Recommended Time & Space Complexity
You should aim for a solution as good or better than O(n * m * t) time and O(n) space, where n is the length of the string s, m is the number of words in wordDict, and t is the maximum length of any word in wordDict.
Hint 1
Try to think of this problem in terms of recursion, where we explore all possibilities. We iterate through the given string s, attempting to pick a word from wordDict that matches a portion of s, and then recursively continue processing the remaining string. Can you determine the recurrence relation and base condition?
Hint 2
The base condition is to return true if we reach the end of the string s. At each recursive call with index i iterating through s, we check all words in wordDict and recursively process the remaining string by incrementing i by the length of the matched word. If any recursive path returns true, we immediately return true. However, this solution is exponential. Can you think of an optimization? Maybe you should consider an approach that avoids repeated work.
Hint 3
We can avoid recalculating results for recursive calls by using memoization. Since we iterate with index i, we can use a hash map or an array of the same length as s to cache the results of recursive calls and prevent redundant computations.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Recursion - Understanding how to break problems into smaller subproblems and handle base cases
- Dynamic Programming - Both memoization (top-down) and tabulation (bottom-up) approaches
- Hash Set - Using sets for O(1) lookup to efficiently check if a word exists in the dictionary
- String Manipulation - Substring operations and string comparison
- Trie (Optional) - A tree-based data structure for efficient prefix matching and word lookup
1. Recursion
Intuition
At every index i in the string, we want to decide:
Can the suffix starting at index
ibe segmented into valid dictionary words?
The recursive idea is:
- Try every word in
wordDict - If a word matches the string starting at position
i - Recursively check whether the remaining substring (starting at
i + len(word)) can also be broken successfully
If any path reaches the end of the string, the answer is true.
This is a classic decision-based recursion where:
- Each index
irepresents a subproblem - Base case: reaching the end means a valid segmentation
Algorithm
- Define a recursive function
dfs(i):- If
i == len(s), returntrue
- If
- For each word
winwordDict:- Check if
wmatchess[i : i + len(w)] - If it matches and
dfs(i + len(w))istrue, returntrue
- Check if
- If no word leads to a valid segmentation, return
false - Start recursion from index
0
Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the string , is the number of words in and is the maximum length of any word in .
2. Recursion (Hash Set)
Intuition
This version improves the brute-force recursion by optimizing word lookup.
Instead of trying every word from wordDict at each index, we:
- Fix a starting index
i - Try all possible substrings
s[i : j+1] - Check if the substring exists in a Hash Set (
O(1)lookup)
If a valid word is found:
- Recursively check whether the remaining suffix starting at
j + 1can be segmented
The key idea:
If we can split the string at any valid word boundary and the rest is solvable, then the whole string is solvable.
Algorithm
- Convert
wordDictinto a hash setwordSetfor fast lookup - Define a recursive function
dfs(i):- If
i == len(s), returntrue
- If
- For every
jfromitolen(s) - 1:- If
s[i : j + 1]is inwordSet- If
dfs(j + 1)istrue, returntrue
- If
- If
- If no split works, return
false - Start recursion from index
0
Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the string and is the number of words in .
3. Dynamic Programming (Top-Down)
Intuition
This is an optimized version of recursion using memoization.
The key observation:
- While recursively checking splits, the same index
iis reached many times - The result of
dfs(i)(can suffixs[i:]be segmented?) never changes
So we cache the result for each index:
- If
dfs(i)was already computed, reuse it - This avoids recomputing exponential subtrees
In short:
Convert exponential recursion into linear states using memoization.
Algorithm
- Use a hash map
memowhere:memo[i] = true/falsemeans whethers[i:]can be segmented- Base case:
memo[len(s)] = true
- Define
dfs(i):- If
iis inmemo, returnmemo[i]
- If
- For each word
winwordDict:- If
s[i : i + len(w)] == w- Recursively call
dfs(i + len(w)) - If it returns
true, storememo[i] = trueand returntrue
- Recursively call
- If
- If no word leads to a valid split:
- Store
memo[i] = false
- Store
- Return
dfs(0)
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
memo = {len(s) : True}
def dfs(i):
if i in memo:
return memo[i]
for w in wordDict:
if ((i + len(w)) <= len(s) and
s[i : i + len(w)] == w
):
if dfs(i + len(w)):
memo[i] = True
return True
memo[i] = False
return False
return dfs(0)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the string , is the number of words in and is the maximum length of any word in .
4. Dynamic Programming (Hash Set)
Intuition
This approach is a top-down dynamic programming solution with pruning.
Key ideas:
- Checking every possible substring is expensive.
- A word can only be as long as the maximum word length in
wordDict. - Use a Hash Set for
O(1)word lookup. - Use memoization so each index in the string is solved only once.
So we:
- Limit how far we try to split from each index
- Cache results for indices to avoid repeated work
This turns exponential recursion into efficient DP.
Algorithm
- Convert
wordDictinto a hash setwordSet - Compute
t= maximum length of any word inwordDict - Use a
memomap wherememo[i]means:- Can substring
s[i:]be segmented?
- Can substring
- Define
dfs(i):- If
iis inmemo, returnmemo[i] - If
i == len(s), returntrue
- If
- For
jfromitomin(len(s), i + t) - 1:- If
s[i : j + 1]is inwordSet- If
dfs(j + 1)istrue, store and returntrue
- If
- If
- If no valid split works:
- Store
memo[i] = false
- Store
- Return
dfs(0)
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
wordSet = set(wordDict)
t = 0
for w in wordDict:
t = max(t, len(w))
memo = {}
def dfs(i):
if i in memo:
return memo[i]
if i == len(s):
return True
for j in range(i, min(len(s), i + t)):
if s[i : j + 1] in wordSet:
if dfs(j + 1):
memo[i] = True
return True
memo[i] = False
return False
return dfs(0)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the string , is the number of words in and is the maximum length of any word in .
5. Dynamic Programming (Bottom-Up)
Intuition
This is a bottom-up dynamic programming approach.
Instead of trying to split the string recursively, we solve the problem from the end of the string toward the start.
Key idea:
dp[i]means whether the substrings[i:]can be segmented- If we know the answer for future positions, we can decide the current one
- We reuse already computed results → no recursion, no stack overhead
Algorithm
- Create a boolean array
dpof sizelen(s) + 1dp[i]=trueifs[i:]can be segmented
- Base case:
dp[len(s)] = true(empty string is valid)
- Iterate
ifromlen(s) - 1down to0:- For each word
winwordDict:- If
s[i : i + len(w)] == w- Set
dp[i] = dp[i + len(w)]
- Set
- If
dp[i]becomestrue, break early
- If
- For each word
- Return
dp[0]
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
dp = [False] * (len(s) + 1)
dp[len(s)] = True
for i in range(len(s) - 1, -1, -1):
for w in wordDict:
if (i + len(w)) <= len(s) and s[i : i + len(w)] == w:
dp[i] = dp[i + len(w)]
if dp[i]:
break
return dp[0]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the string , is the number of words in and is the maximum length of any word in .
6. Dynamic Programming (Trie)
Intuition
The normal DP checks every word at every index, which can waste time comparing strings again and again.
A Trie stores all dictionary words like a prefix tree, so from any starting index i in s, we can walk forward character-by-character and quickly know:
- whether the current prefix matches some dictionary word path
- and when we hit a complete word (
is_word = true)
We still use DP:
dp[i]= can we break the suffixs[i:]into dictionary words?- If from
iwe can reach somejwheres[i..j]is a word, thendp[i] = dp[j+1]
Trie helps us find valid words starting at i efficiently.
Algorithm
- Build a Trie from all words in
wordDict. - Create a boolean DP array
dpof sizen + 1wheren = len(s).dp[n] = true(empty suffix is always valid)
- Let
tbe the maximum word length in the dictionary (use it as a bound). - Fill DP from right to left:
- For each index
ifromndown to0:- Try all end positions
jfromitomin(n-1, i+t-1):- If
s[i..j]is a word in the Trie, setdp[i] = dp[j+1] - If
dp[i]becomestrue, stop early for thisi
- If
- Try all end positions
- For each index
- Return
dp[0].
class TrieNode:
def __init__(self):
self.children = {}
self.is_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_word = True
def search(self, s, i, j):
node = self.root
for idx in range(i, j + 1):
if s[idx] not in node.children:
return False
node = node.children[s[idx]]
return node.is_word
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
trie = Trie()
for word in wordDict:
trie.insert(word)
dp = [False] * (len(s) + 1)
dp[len(s)] = True
t = 0
for w in wordDict:
t = max(t, len(w))
for i in range(len(s), -1, -1):
for j in range(i, min(len(s), i + t)):
if trie.search(s, i, j):
dp[i] = dp[j + 1]
if dp[i]:
break
return dp[0]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the string , is the number of words in and is the maximum length of any word in .
Common Pitfalls
Off-by-One Error in DP Array Initialization
The DP array needs size n + 1 to represent the state after processing all characters. Using size n causes index-out-of-bounds when checking dp[n] as the base case.
# Wrong: array too small
dp = [False] * len(s)
dp[len(s)] = True # IndexError!
# Correct: need n+1 elements
dp = [False] * (len(s) + 1)
dp[len(s)] = TrueChecking Substring Beyond String Length
When iterating through possible word matches, failing to check if the word extends beyond the string causes substring errors or incorrect matches.
# Wrong: may go out of bounds
for w in wordDict:
if s[i:i + len(w)] == w: # no length check
# Correct: verify word fits in remaining string
for w in wordDict:
if i + len(w) <= len(s) and s[i:i + len(w)] == w:Not Converting wordDict to a Set for Efficient Lookup
Using a list for the word dictionary when checking substrings against it results in O(m) lookup time per check, causing TLE on large inputs.
# Wrong: O(m) per lookup
if s[i:j+1] in wordDict: # wordDict is a list
# Correct: O(1) average lookup
wordSet = set(wordDict)
if s[i:j+1] in wordSet:
Sign in to join the discussion