Check whether a string can be split into dictionary words.
Start with the concrete trace below. It shows the state the algorithm must carry as it runs.
Problem trace
Word Break: Only reachable boundaries may launch dictionary words; each full prefix match adds its ending boundary.
Input and goalCheck whether a string can be split into dictionary words.
Initialize boundary 0The empty prefix is reachable, so reachable={0} before scanning any start.
start=0reachable0|0c1a2t3s4a5n6d7o8g9
exampletext = "catsandog", words = ["cats", "dog", "sand", "and", "cat"]positionMeaningboundary after index characters; cell 0 is empty prefixreachable{0}operationinitialize
Match cats at start 0"catsandog" starts with "cats" at 0, so add ending boundary 0+4=4.
start=0reachable0|0c1a2t3reachables4a5n6d7o8g9
exampletext = "catsandog", words = ["cats", "dog", "sand", "and", "cat"]positionMeaningboundary after index characters; cell 0 is empty prefixreachable{0,4}wordcatschecktext[0:4] = "cats"transitionadd 4
Reject dog and sand at start 0Neither "dog" nor "sand" matches the prefix beginning at 0; reachable stays {0,4}.
start=0reachable0|0c1a2t3reachables4a5n6d7o8g9
exampletext = "catsandog", words = ["cats", "dog", "sand", "and", "cat"]positionMeaningboundary after index characters; cell 0 is empty prefixreachable{0,4}wordsdog, sandchecks"cat" != "dog"; "cats" != "sand"transitionno change
Reject and, then match cat"and" misses at 0, but "cat" matches text[0:3], so add boundary 3.
exampletext = "catsandog", words = ["cats", "dog", "sand", "and", "cat"]positionMeaningboundary after index characters; cell 0 is empty prefixreachable{0,3,4}branch1 not reachable: continue
Skip boundary 2Start 2 is not reachable, so no dictionary checks run.
exampletext = "catsandog", words = ["cats", "dog", "sand", "and", "cat"]positionMeaningboundary after index characters; cell 0 is empty prefixreachable{0,3,4}branch2 not reachable: continue
Reach 7 from boundary 3At reachable start 3, cats and dog miss; "sand" matches text[3:7], so add 3+4=7.
exampletext = "catsandog", words = ["cats", "dog", "sand", "and", "cat"]positionMeaningboundary after index characters; cell 0 is empty prefixreachable{0,3,4,7}checkscats miss; dog miss; sand matchestransitionadd 7
Finish checks at boundary 3Words "and" and "cat" also miss at start 3, so reachable remains {0,3,4,7}.
exampletext = "catsandog", words = ["cats", "dog", "sand", "and", "cat"]positionMeaningboundary after index characters; cell 0 is empty prefixreachable{0,3,4,7}checkscats miss; dog miss; sand miss; and matchestransitionadd 7 (already present)
Finish checks at boundary 4"cat" misses at start 4, so no new boundary is added.
exampletext = "catsandog", words = ["cats", "dog", "sand", "and", "cat"]positionMeaningboundary after index characters; cell 0 is empty prefixreachable{0,3,4,7}checkcat misstransitionno change
Skip boundaries 5 and 6Starts 5 and 6 are absent from reachable, so both iterations continue immediately.
exampletext = "catsandog", words = ["cats", "dog", "sand", "and", "cat"]positionMeaningboundary after index characters; cell 0 is empty prefixreachable{0,3,4,7}branches5 not reachable; 6 not reachable
Fail every word at boundary 7Start 7 is reachable, but suffix "og" begins with none of cats, dog, sand, and, or cat.
exampletext = "catsandog", words = ["cats", "dog", "sand", "and", "cat"]positionMeaningboundary after index characters; cell 0 is empty prefixreachable{0,3,4,7}checkscats miss; dog miss; sand miss; and miss; cat misstransitionno change
Skip boundaries 8 and 9Neither 8 nor 9 is reachable, so the scan ends with reachable={0,3,4,7}.
exampletext = "catsandog", words = ["cats", "dog", "sand", "and", "cat"]positionMeaningboundary after index characters; cell 0 is empty prefixreachable{0,3,4,7}branches8 not reachable; 9 not reachable
Reject the full segmentationlen(text)=9 is not in reachable, so "catsandog" cannot be split entirely into dictionary words.
exampletext = "catsandog", words = ["cats", "dog", "sand", "and", "cat"]positionMeaningboundary after index characters; cell 0 is empty prefixreachable{0,3,4,7}comparison9 not in {0,3,4,7}resultfalse
Recognize it
The string must be segmented into reusable dictionary words, so each valid prefix endpoint can seed another exact prefix match.
Keep true
Before scanning start s, reachable contains exactly the boundaries proven segmentable using dictionary words from earlier starts; only members may create new endpoints.
Reuse it
Treat partial solutions as reachable boundaries and propagate only from proven states; this transfers to sentence segmentation, path reachability, and parsing with reusable tokens.
Read it this way: The empty prefix is reachable, so reachable={0} before scanning any start. Step through the frames to watch the state change. The last frame shows the answer or the stopping condition.
Pattern: DP over reachable string positions.
Simple idea: Position 0 is reachable before using any word. From each reachable
position, try every dictionary word. A matching word makes its ending position
reachable.
def word_break(text: str, words: list[str]) -> bool: reachable = {0} for start in range(len(text) + 1): if start not in reachable: continue for word in words: if text.startswith(word, start): reachable.add(start + len(word)) return len(text) in reachable
Cost:O(nwk) time and O(n) space, where w is the number of words and k is the
largest word length.