50. Word Break
Problem
A tokenizer-validation tool needs to sanity-check that a raw, space-stripped string
reconstructed from a detokenization step can actually be re-split into a sequence of
tokens drawn only from the known vocabulary. Given a string s and a list
of vocabulary tokens word_dict, determine whether s can be
segmented into one or more of those tokens, concatenated in order (tokens may repeat).
Source: src/50_word_break.py
def can_segment(s: str, word_dict: list[str]) -> bool: ...
>>> can_segment("tokenizer", ["token", "izer"])
True
>>> can_segment("tokenbroken", ["token", "izer"])
False
Step-by-Step Approach
- Define
dp[i]as "cans[:i](the firsticharacters) be fully segmented using dictionary words." The answer to the whole problem isdp[n], wheren = len(s). - Base case:
dp[0] = True— the empty prefix is trivially segmentable (zero tokens needed). - For each end position
ifrom1ton, try every dictionary word as a candidate for the LAST token ending ati. - For a candidate word of length
len(word), the token would start atj = i - len(word). It's a valid choice exactly whenj >= 0,dp[j]is alreadyTrue(the prefix before the token is itself segmentable), ands[j:i] == word(the characters actually match). - The moment any candidate word satisfies all three conditions, set
dp[i] = Trueand stop checking further words for thisi— one valid last token is enough. - Return
dp[n].
The key insight is that segmentability is compositional: s[:i] is
segmentable if and only if there's SOME split point where everything before it is
already known to be segmentable AND the remaining suffix up to i is
itself a single dictionary word — which is exactly what makes this a 1D DP over string
prefixes rather than needing to enumerate every possible full segmentation.
Reference solution
def can_segment(s: str, word_dict: list[str]) -> bool:
# dp[i] = whether s[:i] can be fully segmented; for each end position,
# try every dict word as the LAST token and check the remaining prefix.
# O(n * len(word_dict) * max_word_len) time, O(n) space.
words = set(word_dict)
n = len(s)
dp = [False] * (n + 1)
dp[0] = True # empty prefix is trivially segmentable
for i in range(1, n + 1):
for word in words:
j = i - len(word)
if j >= 0 and dp[j] and s[j:i] == word:
dp[i] = True
break # one valid last-token choice is enough for this i
return dp[n]
Key Functions & Tricks
set(word_dict)— deduplicates the dictionary and gives fast membership semantics, though the loop here still checks each word explicitly to find its length and matching substring.dp[i - 1]-style prefix DP — building the answer for positionistrictly from already-computed smaller positions.s[j:i] == word— the actual substring-equality check that confirms a candidate word truly occurs at that position.breakon the first valid word — short-circuits the inner loop oncedp[i]is known to beTrue, since further checks can't change the answer.dp = [False] * (n + 1)— the+1sizing lets index0cleanly represent the empty prefix, avoiding off-by-one special casing.
How to Recognize This Pattern
Signal words: "word break," "can this string be segmented into dictionary words," "split a string into valid tokens/parts from a given set." The tell is needing to know if a full string can be partitioned into pieces that are each individually valid, where pieces can be reused and boundaries aren't given — that's a 1D prefix-DP over string positions, not a greedy left-to-right scan (greedy fails whenever an early valid-looking token choice blocks a later valid segmentation). Common variations: Word Break II, which returns ALL valid segmentations instead of just a boolean (same DP shape, but building up lists of strings instead of booleans, often with memoized backtracking); or capping token length by the longest dictionary word, which bounds the inner loop instead of scanning every dictionary word at every position. A common pitfall is trying to greedily match the longest possible token first — that can commit to a split that makes the rest of the string unsegmentable even when a different split would have worked.