Given a string s and a dictionary of strings wordDict, return true if s can be segmented into a space-separated sequence of one or more dictionary words.
Note that the same word in the dictionary may be reused multiple times in the segmentation.
OR
Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separated sequence of one or more dictionary words.
Input: s ="leetcode", wordDict =["leet","code"]Output: trueExplanation: Return true because "leetcode" can be segmented as "leet code".
Example 2:
1
2
3
4
Input: s ="applepenapple", wordDict =["apple","pen"]Output: trueExplanation: Return true because "applepenapple" can be segmented as "apple pen apple".Note that you are allowed to reuse a dictionary word.
Example 3:
1
2
Input: s ="catsandog", wordDict =["cats","dog","sand","and","cat"]Output: false
Now, in terms of solution, one of the naive approach is
You generate all possible substrings of the string (using two nested loops).
For each substring, check if it exists in the dictionary.
If a substring matches, for e.g. substring apple matches, then we recursively check the remaining part of the string. which is penapple and repeat the same process.
However, in this way we will be generating a lot of unnecessary substrings. For instance, a substring like "appl" or "ppl (in the middle of "applepenapple") may never be useful if it’s not a match in the dictionary.
Method 2 - Still naive - Try to break the string using all prefixes#
publicclassSolution {
publicbooleanwordBreak(String s, List<String> wordDict) {
// Convert wordDict to a set for quicker lookups.return dfs(s, new HashSet<String>(wordDict), 0);
}
privatebooleandfs(String s, Set<String> set, int idx) {
// Base case: If the index reaches the end of the string, successful segmentation.if (idx == s.length()) {
returntrue;
}
// Try all substrings starting from idx.for (int i = idx + 1; i <= s.length(); i++) {
// Check if the substring is valid and recursively check the remaining part.if (set.contains(s.substring(idx, i)) && dfs(s, set, i)) {
returntrue;
}
}
returnfalse; // No valid segmentation found. }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
classSolution:
defwordBreak(self, s: str, wordDict: List[str]) -> bool:
dict_set = set(wordDict) # Convert wordDict to a set for faster word lookups n = len(s)
defdfs(idx: int) -> bool:
if idx == n:
returnTrue# Reached the end of the string, successful segmentation# Try all substrings starting from idxfor i in range(idx +1, n +1):
if s[idx:i] in dict_set and dfs(i):
returnTruereturnFalsereturn dfs(0)
This is very similar to method 2, but we can also loop on wordDict to do the break.
As dictionary size is smaller than string s, we can loop over all the words in dict. For word w in dictionary, we can check if w exists in string and look for remaining string to break.
In method 1, if the size of the dictionary is very large, the time is bad. Instead we can solve the problem in O(n^2) time (n is the length of the string).
For example, for string abcd the recursion tree will look like:
graph TD
A["applepenapple"] -->|a| N1["pplepenapple ❌"]
A -->|"..."| N2["❌"]
A -->|apple| B["penapple"]
A -->|...| C["❌"]
B -->|p| D["enapple❌"]
B -->|...| N3["❌"]
B -->|pen| E["apple"]
B -->|...| N4["❌"]
E -->|a| N5["pple❌"]
E -->|...| N6["❌"]
E -->|apple| F["End of string → ✅"]
We can see that subproblems for “cd” is repeating twice. So, we can cache the solution.
The recursive solution has many subproblems that are repeating at various stage of the recursion.
For instance, consider the case where s = "catsanddog" and wordDict = ["cat", "cats", "and", "sand", "dog"]. Suppose we are trying to break the string into words:
If we break it as "cat" followed by "sand", or as "cats" followed by "and", the remaining part of the string, "dog", is shared by both paths.
Since "dog" exists in the dictionary and can be successfully segmented, we should not need to recompute its result for both paths. Instead, we reuse the result (true), which saves us redundant calculations.
classSolution:
defwordBreak(self, s: str, wordDict: List[str]) -> bool:
dict_set = set(wordDict) # Convert wordDict to a set for faster word lookups n = len(s)
memo = [None] * n # Use a boolean array, initialised to None (uncomputed states)defdfs(idx: int) -> bool:
if idx == n:
returnTrue# Reached the end of the string, successful segmentationif memo[idx] isnotNone: # Check cachereturn memo[idx]
# Try all substrings starting from idxfor i in range(idx +1, n +1):
if s[idx:i] in dict_set and dfs(i):
memo[idx] =True# Cache the resultreturnTrue memo[idx] =False# Cache the resultreturnFalsereturn dfs(0)
⏰ Time complexity: O(n^3 + m). Where n - length of string s and m - number of words in dict. We have n paths in dfs, and then we loop from idx + 1 to n, and then we create substring which takes O(n) time, hence overall O(n^3), plus O(m) time to make set.
🧺 Space complexity: O(n + m) - for recursive state, and m for storing values in set.
We can convert previous approach into bottom-up DP. Lets dp[i] represents if substring(0,i) is breakable. Now, for each substring, we will check and fill in the dp[i].
classSolution {
publicbooleanwordBreak(String s, List<String> wordDict) {
Set<String> dict =new HashSet<>(wordDict); // Convert wordDict to a set for fast lookupsint n = s.length();
boolean[] dp =newboolean[n + 1]; // dp[i] indicates if s[0:i] can be segmented dp[0]=true; // Base case: empty string can always be segmentedfor (int i = 1; i <= n; i++) { // Iterate over substring lengthsfor (int j = 0; j < i; j++) { // Check all partitions for s[0:i]if (dp[j]&& dict.contains(s.substring(j, i))) {
dp[i]=true;
break; // No need to check further partitions for this i }
}
}
return dp[n]; // Return whether the entire string can be segmented }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
classSolution:
defwordBreak(self, s: str, wordDict: List[str]) -> bool:
n = len(s)
dp = [False] * (n +1) # dp[i] indicates if s[0:i] can be segmented dp[0] =True# Base case: empty string can always be segmentedfor i in range(1, n +1): # Iterate over substring lengthsfor j in range(i): # Check all possible partitionsif dp[j] and s[j:i] in dict_set:
dp[i] =Truebreak# No need to check further partitions for this ireturn dp[n] # Whether the entire string can be segmented
If the substring s[i:j] (starting at node i and ending at node j) exists in the dictionary, then there is a valid edge between nodes i and j.
The problem reduces to checking if there exists a path from the start node (0) to the end node (n).
The BFS approach ensures we traverse these “edges” systematically and avoid revisiting nodes using a visited set, which optimises the process and prevents redundant calculations.
classSolution {
publicbooleanwordBreak(String s, List<String> wordDict) {
Set<String> dict =new HashSet<>(wordDict); // Convert wordDict to a set for fast lookupsint n = s.length();
Queue<Integer> queue =new LinkedList<>(); // Queue to track starting indicesboolean[] visited =newboolean[n + 1]; // Visited array to avoid redundant checks queue.offer(0); // Start BFS from index 0while (!queue.isEmpty()) {
int start = queue.poll(); // Get the next index to processif (visited[start]) continue; // Skip if already visited visited[start]=true;
for (int end = start + 1; end <= n; end++) { // Explore substrings starting from 'start'if (dict.contains(s.substring(start, end))) { // Check if substring exists in wordDictif (end == n) returntrue; // If we reach the end of the string, return true queue.offer(end); // Enqueue the next start index }
}
}
returnfalse; // If the queue is empty and we haven't reached the end, return false }
}
classSolution:
defwordBreak(self, s: str, wordDict: List[str]) -> bool:
dict_set = set(wordDict) # Convert wordDict to a set for fast lookups n = len(s)
queue = deque([0]) # Queue to track starting indices visited = [False] * (n +1) # Visited array to avoid redundant checkswhile queue:
start = queue.popleft() # Get the next index to processif visited[start]: # Skip if already visitedcontinue visited[start] =Truefor end in range(start +1, n +1): # Explore substrings starting from 'start'if s[start:end] in dict_set: # Check if substring exists in wordDictif end == n: # If we reach the end of the string, return truereturnTrue queue.append(end) # Enqueue the next start indexreturnFalse# If the queue is empty and we haven't reached the end, return false
The Trie is an efficient way to store and search for word prefixes, allowing us to quickly match substrings of s against words in the dictionary while traversing the string.
classTrieNode {
Map<Character, TrieNode> children =new HashMap<>();
boolean isWord =false; // Marks the end of a word}
classSolution {
private TrieNode root =new TrieNode(); // Root node of TriepublicbooleanwordBreak(String s, List<String> wordDict) {
// Build the Trie buildTrie(wordDict);
// Use DFS to check if the string can be segmentedint n = s.length();
boolean[] visited =newboolean[n + 1]; // Prevent re-visiting indicesreturn dfs(s, 0, visited);
}
// Insert all words from wordDict into the TrieprivatevoidbuildTrie(List<String> wordDict) {
for (String word : wordDict) {
TrieNode node = root;
for (char c : word.toCharArray()) {
node.children.putIfAbsent(c, new TrieNode()); // Add child node if absent node = node.children.get(c);
}
node.isWord=true; // Mark the end of a word }
}
// DFS to segment the stringprivatebooleandfs(String s, int start, boolean[] visited) {
if (start == s.length()) {
returntrue; // Successfully segmented the entire string }
if (visited[start]) {
returnfalse; // Avoid redundant checks }
visited[start]=true; // Mark this index as visited TrieNode node = root;
for (int i = start; i < s.length(); i++) {
char c = s.charAt(i);
if (!node.children.containsKey(c)) {
break; // Stop traversing if no further match }
node = node.children.get(c); // Move to the next TrieNodeif (node.isWord&& dfs(s, i + 1, visited)) { // Check the remaining substring recursivelyreturntrue;
}
}
returnfalse; // No valid segmentation found }
}
classTrieNode:
def__init__(self):
self.children = {}
self.is_word =False# Marks the end of a wordclassSolution:
defwordBreak(self, s: str, wordDict: List[str]) -> bool:
# Build the Trie root = self.buildTrie(wordDict)
# Use DFS to check if the string can be segmented n = len(s)
visited = [False] * (n +1) # Prevent re-visiting indicesreturn self.dfs(s, 0, visited, root)
# Insert all words from wordDict into the TriedefbuildTrie(self, wordDict: List[str]) -> TrieNode:
root = TrieNode()
for word in wordDict:
node = root
for c in word:
if c notin node.children:
node.children[c] = TrieNode() # Add child node if absent node = node.children[c]
node.is_word =True# Mark the end of a wordreturn root
# DFS to segment the stringdefdfs(self, s: str, start: int, visited: List[bool], root: TrieNode) -> bool:
if start == len(s):
returnTrue# Successfully segmented the entire stringif visited[start]:
returnFalse# Avoid redundant checks visited[start] =True# Mark this index as visited node = root
for i in range(start, len(s)):
c = s[i]
if c notin node.children:
break# Stop traversing if no further match node = node.children[c] # Move to the next TrieNodeif node.is_word and self.dfs(s, i +1, visited, root): # Check the remaining substring recursivelyreturnTruereturnFalse# No valid segmentation found