Topics › Recursion and Backtracking › Trying out all Combos / Hard
Word Break
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given a string s and a list of words wordDict. Decide whether s can be written as a sequence of one or more dictionary words placed back to back. The same word may be used any number of times, and the words in the dictionary are distinct and non-empty.
Constraints: 1 ≤ s.length ≤ 300, 1 ≤ wordDict.length ≤ 1000, 1 ≤ wordDict[i].length ≤ 20, lowercase letters only.
Example 1
s = "leetcode", wordDict = ["leet", "code"]
answer = true // "leet" + "code"
Example 2
s = "applepenapple", wordDict = ["apple", "pen"]
answer = true // "apple" + "pen" + "apple"; reuse is allowed
Example 3
s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
answer = false // "cats"+"and" leaves "og"; "cat"+"sand" leaves "og"
Approach 1: Brute force, recursive splitting
Intuition. Whatever the segmentation looks like, its first piece is a dictionary word that is a prefix of s. So: try each dictionary word; if it matches at the current position, the question reduces to “can the rest be segmented?”, which is the same question on a shorter string. The empty remainder is trivially segmented. This is a plain recursive search over all possible first words.
Greedy does not work here (Example 3 shows two plausible first words that both fail, and "cars" with ["car", "ca", "rs"] shows the longest-match choice failing while a shorter one succeeds), so every alternative has to be explored.
Algorithm.
canBreakFrom(start): ifstart == s.length()returntrue.- For each word
w: ifsstarts withwatstartandcanBreakFrom(start + w.length()), returntrue. - Return
false.
static boolean bruteForce(String s, List<String> wordDict) {
return canBreakFrom(s, 0, wordDict);
}
private static boolean canBreakFrom(String s, int start, List<String> dict) {
if (start == s.length()) return true; // everything before `start` was covered by words
for (String w : dict) {
if (!w.isEmpty() && s.startsWith(w, start) && canBreakFrom(s, start + w.length(), dict)) return true;
}
return false;
}
Complexity. Time O(2^n · k) in the worst case, where k is the dictionary size: the string "aaaa...ab" with the dictionary ["a", "aa", "aaa"] makes the recursion revisit the same suffix through every composition of the prefix, and all of them eventually fail on the b. Space O(n) for the recursion depth.
The !w.isEmpty() guard protects against an empty dictionary word, which would recurse at the same start for ever.
Approach 2: Better, memoise the start index
Intuition. The brute force is slow only because it solves the same subproblem again and again: the answer for canBreakFrom(start) depends on nothing but start. There are only n + 1 distinct values of start, so caching the result for each turns an exponential tree into at most n + 1 real computations. This is top-down dynamic programming; the recursion is unchanged, it just remembers.
Putting the words in a HashSet lets us ask “is s[start, end) a word?” for each end in O(1) expected time instead of looping over the dictionary.
Algorithm.
- Build
dictas aHashSet, allocateBoolean[] memoof sizen + 1(nullmeans unknown). canBreakMemo(start): returntrueatn; returnmemo[start]if it is set.- For each
end > start: ifs[start, end)is indictand the suffix fromendcan be broken, store and returntrue. - Store and return
false.
static boolean better(String s, List<String> wordDict) {
Set<String> dict = new HashSet<>(wordDict);
Boolean[] memo = new Boolean[s.length() + 1]; // null = not computed yet
return canBreakMemo(s, 0, dict, memo);
}
private static boolean canBreakMemo(String s, int start, Set<String> dict, Boolean[] memo) {
if (start == s.length()) return true;
if (memo[start] != null) return memo[start];
for (int end = start + 1; end <= s.length(); end++) {
if (dict.contains(s.substring(start, end)) && canBreakMemo(s, end, dict, memo)) {
memo[start] = true;
return true;
}
}
memo[start] = false;
return false;
}
Complexity. Time O(n² · n) = O(n³) in the worst case: n memo entries, each trying O(n) end positions, each building and hashing a substring of length up to n. Space O(n) for the memo and recursion, plus the dictionary set.
Approach 3: Optimal, bottom-up DP bounded by the longest word
Intuition. Turn the memo around. Let dp[i] be “the prefix s[0, i) can be segmented”. dp[0] is true (empty prefix). For i > 0, the prefix is segmentable exactly when its last word is some s[j, i) in the dictionary and the prefix before it, s[0, j), is itself segmentable: dp[i] = OR over j of (dp[j] AND s[j, i) ∈ dict). Filling i from 1 to n guarantees every dp[j] we read is final.
The bound that makes this fast: a word of length more than maxLen (the longest dictionary word) can never match, so j only needs to go back maxLen positions. With maxLen ≤ 20 the inner loop is tiny regardless of n.
Algorithm.
maxLen= longest word length;dp[0] = true.- For
ifrom1ton, forjfromi - 1down tomax(0, i - maxLen): ifdp[j]ands[j, i)is a word, setdp[i] = trueand stop the inner loop. - Return
dp[n].
static boolean optimal(String s, List<String> wordDict) {
Set<String> dict = new HashSet<>(wordDict);
int maxLen = 0;
for (String w : dict) maxLen = Math.max(maxLen, w.length());
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true; // the empty prefix is trivially segmented
for (int i = 1; i <= n; i++) {
for (int j = i - 1; j >= 0 && i - j <= maxLen; j--) {
if (dp[j] && dict.contains(s.substring(j, i))) {
dp[i] = true;
break; // one valid last word is enough
}
}
}
return dp[n];
}
Dry run on s = "leetcode", dict = {leet, code}, maxLen = 4:
| i | prefix s[0, i) |
candidate last words s[j, i) with dp[j] true |
dp[i] |
|---|---|---|---|
| 1 | l |
l |
false |
| 2 | le |
le |
false |
| 3 | lee |
lee |
false |
| 4 | leet |
leet (j = 0) |
true |
| 5 | leetc |
c (j = 4) |
false |
| 6 | leetco |
co (j = 4) |
false |
| 7 | leetcod |
cod (j = 4) |
false |
| 8 | leetcode |
code (j = 4) |
true |
Only j values with dp[j] == true and i - j ≤ 4 matter, which is why rows 5 to 8 list a single candidate each: j = 0 is too far back once i > 4.
Complexity. Time O(n · L · L) where L = maxLen: n positions, at most L candidate j, each substring of length at most L. With the problem’s bounds that is well under a million operations. Space O(n) for dp plus the set.
Edge cases and pitfalls
- Empty string. The empty string is vacuously segmentable; all three methods return
true(dp[0], orstart == lengthat once). LeetCode never asks it, but the code should not crash on it. - Word reuse.
"applepenapple"needsappletwice. Nothing in the algorithms removes a word after use, which is correct. - Greedy longest match fails.
"cars"with["car", "ca", "rs"]: takingcarstrandss; the DP triescaas well. - Exponential inputs.
"aaaaaaaaaaaab"with["a", "aa", "aaa"]isfalseand makes the brute force explore roughly a tribonacci number of splits; memoisation collapses this toO(n²)substring checks. - Substrings and hashing.
s.substring(j, i)allocates; in Java 21 that isO(i - j). Keeping the inner loop bounded bymaxLenis what keeps the allocations small. - Empty dictionary word. Would make the recursion loop at the same index. The brute force skips it explicitly; the set-based versions never match it because
end > start. - Only a boolean is required. To list all segmentations instead, see the print-all-ways variant linked below; the DP table tells you which prefixes are worth extending.
Related problems
- Word Break (print all ways) asks for every segmentation, so the backtracking must continue after the first success.
- Palindrome partitioning cuts the string with a different predicate (is this piece a palindrome?) and lists all cuts.
- Subset sum equal to target (DP- 14) is another take-or-skip recursion that memoisation turns into a table.
- Longest Word with All Prefixes uses a trie for prefix queries, an alternative to the hash set here when the dictionary is huge.