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 <= 20s and wordDict[i] consist of only lowercase English letters.
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.
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?
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.
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.
Before attempting this problem, you should be comfortable with:
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:
wordDictii + len(word)) can also be broken successfullyIf any path reaches the end of the string, the answer is true.
This is a classic decision-based recursion where:
i represents a subproblemdfs(i):i == len(s), return truew in wordDict:w matches s[i : i + len(w)]dfs(i + len(w)) is true, return truefalse0Where is the length of the string , is the number of words in and is the maximum length of any word in .
This version improves the brute-force recursion by optimizing word lookup.
Instead of trying every word from wordDict at each index, we:
is[i : j+1]O(1) lookup)If a valid word is found:
j + 1 can be segmentedThe key idea:
If we can split the string at any valid word boundary and the rest is solvable, then the whole string is solvable.
wordDict into a hash set wordSet for fast lookupdfs(i):i == len(s), return truej from i to len(s) - 1:s[i : j + 1] is in wordSetdfs(j + 1) is true, return truefalse0Where is the length of the string and is the number of words in .
This is an optimized version of recursion using memoization.
The key observation:
i is reached many timesdfs(i) (can suffix s[i:] be segmented?) never changesSo we cache the result for each index:
dfs(i) was already computed, reuse itIn short:
Convert exponential recursion into linear states using memoization.
memo where:memo[i] = true/false means whether s[i:] can be segmentedmemo[len(s)] = truedfs(i):i is in memo, return memo[i]w in wordDict:s[i : i + len(w)] == wdfs(i + len(w))true, store memo[i] = true and return truememo[i] = falsedfs(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)Where is the length of the string , is the number of words in and is the maximum length of any word in .
This approach is a top-down dynamic programming solution with pruning.
Key ideas:
wordDict.O(1) word lookup.So we:
This turns exponential recursion into efficient DP.
wordDict into a hash set wordSett = maximum length of any word in wordDictmemo map where memo[i] means:s[i:] be segmented?dfs(i):i is in memo, return memo[i]i == len(s), return truej from i to min(len(s), i + t) - 1:s[i : j + 1] is in wordSetdfs(j + 1) is true, store and return truememo[i] = falsedfs(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)Where is the length of the string , is the number of words in and is the maximum length of any word in .
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 substring s[i:] can be segmenteddp of size len(s) + 1dp[i] = true if s[i:] can be segmenteddp[len(s)] = true (empty string is valid)i from len(s) - 1 down to 0:w in wordDict:s[i : i + len(w)] == wdp[i] = dp[i + len(w)]dp[i] becomes true, break earlydp[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]Where is the length of the string , is the number of words in and is the maximum length of any word in .
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:
is_word = true)We still use DP:
dp[i] = can we break the suffix s[i:] into dictionary words?i we can reach some j where s[i..j] is a word, then dp[i] = dp[j+1]Trie helps us find valid words starting at i efficiently.
wordDict.dp of size n + 1 where n = len(s).dp[n] = true (empty suffix is always valid)t be the maximum word length in the dictionary (use it as a bound).i from n down to 0:j from i to min(n-1, i+t-1):s[i..j] is a word in the Trie, set dp[i] = dp[j+1]dp[i] becomes true, stop early for this idp[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]Where is the length of the string , is the number of words in and is the maximum length of any word in .
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)] = TrueWhen 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: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