Intuition
Define dp[i] = True iff the prefix s[:i] can be segmented into dictionary words. The last word in any valid segmentation ends at position i, so it starts at some earlier position j where dp[j] was already True and s[j:i] is a dictionary word. Try every split point.
Explanation
The recurrence
dp[i] is True iff there exists some j with 0 ≤ j < i such that:
dp[j]isTrue(the prefix up tojis segmentable), ands[j:i]is a word in the dictionary.
Base case: dp[0] = True — the empty prefix is trivially segmentable.
Steps
- Convert
wordDictinto asetfor O(1) lookup. - Initialise
dp = [False] * (n + 1)and setdp[0] = True. - For
ifrom1ton:- For
jfrom0toi - 1:- If
dp[j]ands[j:i]is in the dictionary, setdp[i] = Trueand break the inner loop.
- If
- For
- Return
dp[n].
Walk-through
s = "bookcase", wordDict = ["book", "case"].
| i | considering j | dp[i] |
|---|---|---|
| 0 | (base case) | True |
| 1 | j=0: s[0:1]="b" ∉ dict | False |
| 2 | j=0: "bo" ∉; j=1: skip (dp[1]=F) | False |
| 3 | similar, no match | False |
| 4 | j=0: "book" ∈ dict, dp[0]=True → dp[4]=True | True |
| 5 | j=4: "c" ∉; others skip | False |
| 6 | j=4: "ca" ∉; others skip | False |
| 7 | j=4: "cas" ∉; others skip | False |
| 8 | j=4: "case" ∈ dict, dp[4]=True → dp[8]=True | True |
Return dp[8] = True. ✓
s = "applepenapple", wordDict = ["apple", "pen"] → walk produces dp[5] = True (apple), dp[8] = True (apple+pen), dp[13] = True (+apple). Return True.
s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] → no segmentation reaches dp[9]. Return False.
Why we break early on a match
Once we find a valid split for dp[i], we don't need to keep searching — we already know dp[i] = True. The break trims the inner loop at the first hit, useful for sparse-match strings.
Edge cases
- Empty string:
dp[0] = True, returnTrue. - No valid split:
dp[n] = False, returnFalse. - Single-letter words: still O(n²) substring checks; the algorithm handles it naturally.
Optimisation
Instead of iterating j = 0..i-1, we can iterate j = i - L_max..i-1 where L_max is the longest word in the dictionary — since any longer substring can't be a single dictionary word. That bounds the inner loop, useful when L_max ≪ n.
Complexity
- Time:
O(n² · m)worst case — the substrings[j:i]isO(i-j)to construct andO(L)to hash for the set lookup. With sufficient bounds this often runs much faster in practice. - Space:
O(n)fordp, plusO(W)for the word set.
Implementation
import java.util.*;
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> words = new HashSet<>(wordDict);
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < i; j++) {
if (dp[j] && words.contains(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[n];
}
}