Word Break asks whether a string can be chopped into pieces that are all dictionary words. It is the gateway to 1-D dynamic programming — the trick of building one boolean answer per prefix and reusing earlier answers.
Problem. Given a string s and a list of words words, return True if s can be segmented into a
space-separated sequence of one or more words from the list. Words may be reused.
Example: s = "leetcode", words = {leet, code} → True (because "leet" + "code" covers the whole string).
The slow way first
The obvious idea: try every way to cut the string. Pick a first word, then recursively try to break the rest. That works, but the same suffix gets re-solved over and over, so it blows up to exponential time on strings with many possible cuts.
The question to ask: what do I keep re-computing? Whether a given prefix of the string is breakable. If I record that answer once, I never have to redo it. That is exactly what a dp array buys us.
The idea
Let dp[i] mean: "can s[:i] be split into dictionary words?" Seed dp[0] = True — the empty prefix is trivially breakable. Then fill the row left to right. For each end i, look for a split point j where dp[j] is already True and the slice s[j:i] is a dictionary word. If both hold, s[:i] is breakable, so set dp[i] = True.
The key insight: a longer prefix is breakable only if some shorter prefix was breakable and the leftover tail is a single word. We build big answers out of small ones we already trust.
Walk through it
Step through the animation. The top row is the string "leetcode"; the row below it is the dp array. We seed dp[0] = True. At i = 4 we try j = 0: dp[0] is True and s[0:4] = "leet" is a word, so dp[4] = True. For i = 5, 6, 7 no split works, so they stay False. At i = 8 we reach j = 4: dp[4] is True and s[4:8] = "code" is a word, so dp[8] = True. The final answer is dp[8].
Pseudocode
put the words into a set # O(1) "is this slice a word?" lookups
dp = array of False, length len(s)+1
dp[0] = True # empty prefix is breakable
for each end i from 1 to len(s):
for each split point j from 0 to i-1:
if dp[j] is True and s[j:i] is in the set:
dp[i] = True
stop scanning j
return dp[len(s)]The Python solution
def word_break(s, words):
words = set(words)
dp = [False] * (len(s) + 1)
dp[0] = True
for i in range(1, len(s) + 1):
for j in range(i):
if dp[j] and s[j:i] in words:
dp[i] = True
break
return dp[len(s)]words = set(words)turns the list into a set so eachs[j:i] in wordscheck is O(1).dphaslen(s) + 1slots;dp[i]answers "iss[:i]breakable?"dp[0] = Trueis the seed: the empty prefix is always breakable.- The outer loop fixes the end
i; the inner loop scans every split pointj < i. - Line 7 is the heart:
dp[j]must already beTrueand the tails[j:i]must be a word. - When both hold we set
dp[i] = Trueandbreak— once a prefix is breakable, the reason does not matter. - We return
dp[len(s)]: can the whole string be segmented?
Complexity
| Case | Time | Notes |
|---|---|---|
| Naive recursion (re-solves suffixes) | O(2^n) (slow) | exponential branching |
| DP (this solution) | O(n²) slices × cost (slow) | two nested loops plus slicing |
O(n) (moderate)We trade O(n) extra space (the dp array) for a huge speed win over the exponential search. The move — cache one answer per prefix and reuse it — is the core of 1-D dynamic programming.
When this pattern shows up
Whenever a problem asks "can this be built up from smaller valid pieces" — segmenting a string, making change for an amount, reaching a step count — reach for a 1-D dp array indexed by prefix or amount. Seed the empty/zero case, then fill forward, each entry built from earlier ones.
Mind the indexing: dp has length len(s) + 1, not len(s), because dp[i] describes the prefix of
length i and you need a slot for the full string. Forgetting the + 1 is the classic off-by-one here.
Practice
For s = 'leetcode', when the end i reaches 8, which split point j wins and why?
1. What does dp[i] represent?
2. Why is dp[0] seeded to True?
3. What must be true to set dp[i] = True for some split point j?
4. Why convert the word list into a set?