140. Word Break II
On LeetCode ->Problem¶
Insert spaces into s so every segment is in wordDict; return every valid sentence, and allow words to be reused.
s = "catsanddog"
wordDict = ["cat", "cats", "and", "sand", "dog"]
output = ["cat sand dog", "cats and dog"]
Key trick¶
Memoize all valid decompositions of each suffix index; the empty suffix has one decomposition, the empty sequence of words.
Trap¶
- Forgetting the empty base decomposition prevents the first word from being added.
- Backtracking without memoization repeatedly solves the same suffix.
- Returning a string instead of a list of sentences.
- Assuming the number of answers is polynomial; enumeration can be exponential.
Why is it interesting?¶
It combines dynamic programming with output generation: feasibility alone is easy, but every valid path must also be reconstructed.
Python solution¶
from functools import cache
class Solution:
def wordBreak(self, s: str, wordDict: list[str]) -> list[str]:
words = set(wordDict)
n = len(s)
lengths = {len(word) for word in words}
@cache
def dfs(i):
# One empty decomposition completes a sentence.
if i == n:
return [()]
res = []
for length in lengths:
j = i + length
if j <= n:
word = s[i:j]
if word in words:
# Prepend this word to every decomposition of the suffix.
for suffix in dfs(j):
res.append((word, *suffix))
return res
return [" ".join(parts) for parts in dfs(0)]
- Time: \(O(n2^n)\) in the worst case, including construction of all answers.
- Space: \(O(n2^n)\) for memoized decompositions and returned answers.
Comment on my solution¶
The dynamic programming direction is reasonable, but several indexing and type errors prevent it from working:
- A prefix DP is simpler with
n + 1states anddp[0]initialized with one empty decomposition. kis a zero-based bucket index, so the corresponding word length isk + 1.- The condition
j > 0excludes words beginning at index zero. - The slice has the wrong endpoint because
kis not the word length. s[...]is compared withwords_k, which is a list rather than one word.word_kis undefined; the loop variable isword.- Assigning to
dp[i]overwrites sentences found through earlier matches. " ".join(dp[-1])cannot join lists of words and returns one string rather than a list of sentences.
# WRONG
# I think the logic is correct but the indices are wrong
class Solution:
def wordBreak(self, s: str, wordDict: list[str]) -> list[str]:
# "catsanddog", ["cat","cats","and","sand","dog"]
# -> ["cats and dog","cat sand dog"]
# "catsandog", ["cat","cats","and","sand","dog"]
# -> []
n = len(s)
# dp[i] = list of all possible word combination whose concatenation
# matches s[:i+1]
dp = [[] for _ in range(n)]
max_word_len = max(len(word) for word in set(wordDict))
words = [[] for _ in range(max_word_len)]
for word in set(wordDict):
words[len(word) - 1].append(word)
for i in range(n):
for k, words_k in enumerate(words):
for word in words_k:
j = i - k
if j > 0 and s[j:j + k] == words_k:
dp[i] = [prev + [word_k] for prev in dp[j]]
return " ".join(dp[-1])