Word Break
Decide, for every prefix of the string, whether it splits into dictionary words, using the answers for shorter prefixes. O(n × m × k) time and O(n) space, with a step-through, code in five languages and the Big O worked out from the code.
The problem
Section titled “The problem”You are given a string text and a list of words wordDict. Return true if text can be cut into pieces
so that every piece is a word from the list. A word may be used any number of times.
Example 1
Input: text = "carpets", wordDict = ["carp", "car", "pets", "et"]
Output: true
“car” + “pets”. Starting with “carp” looks promising, but “carp” + “et” leaves an “s” that no word covers.
Example 2
Input: text = "pencil", wordDict = ["pen", "cill"]
Output: false
After “pen”, the rest is “cil”, which is not a word.
Example 3
Input: text = "toptop", wordDict = ["top"]
Output: true
The same word can be used twice.
Constraints
Section titled “Constraints”- 1 ≤ text.length ≤ 300 (call it n)
- 1 ≤ wordDict.length ≤ 1000 (call it m)
- 1 ≤ wordDict[i].length ≤ 20 (call the longest k)
- Only lowercase English letters, and the words are all different.
Clarifying questions
Section titled “Clarifying questions”- Can a word be used more than once?
- Yes, as often as needed.
- Must every letter be used?
- Yes. The pieces must cover the whole string with nothing left over.
- Do we return the split?
- No, only whether one exists. (Word Break II asks for all of them.)
- Is the first matching word always safe to take?
- No. In Example 1, "carp" matches but leads nowhere.
Spot the pattern
Section titled “Spot the pattern”Look at the last word of a split. If the string ends with some word w, the question becomes: does the part before w split? That is the same question on a shorter prefix, and many choices lead to the same prefix. Answering each prefix once and reusing it is dynamic programming.
Solutions
Section titled “Solutions”There is no middle tab. Adding a memo to the recursion reaches the same O(n × m × k) as the table. A trie of the words can speed up the matching, but the idea stays the same.
Plain recursion O(2ⁿ × m × k)
The idea
Section titled “The idea”Let can(position) ask: can the letters from position to the end be split into words? If position is the end, yes.
Otherwise, try every word. If the string has that word at position, take it and ask can(position + len(word)).
If any choice works, return true. If none does, return false, and the caller tries its next word.
Why it is correct: every split begins with some word at position 0, and the search tries every word there, then every word after it, and so on. It only gives up on a choice once everything after it has failed.
Walkthrough
Section titled “Walkthrough”- def can(position):
- if position == len(text): return True
- for word in words:
- if text starts with word at position:
- if can(position + len(word)): return True
- return False
- return can(0)
can(position) asks: can the letters from position to the end be split into words? Try each word that matches at position, and ask the same about the rest. Words: carp, car, pets, et.
class Solution { private String text; private List<String> words;
// Plain recursion: try every word that matches at the current position, and // solve the rest after it. A dead end is retried from scratch on every path that reaches it. public boolean wordBreak(String text, List<String> wordDict) { this.text = text; this.words = wordDict; return can(0); }
private boolean can(int position) { // Reached the end: every letter is covered by a word. if (position == text.length()) return true; for (String word : words) { if (text.startsWith(word, position) && can(position + word.length())) return true; } return false; }}# Plain recursion: try every word that matches at the current position, and# solve the rest after it. A dead end is retried from scratch on every path that reaches it.def word_break(text: str, word_dict: list[str]) -> bool: def can(position: int) -> bool: # Reached the end: every letter is covered by a word. if position == len(text): return True for word in word_dict: if text.startswith(word, position) and can(position + len(word)): return True return False
return can(0)public class Solution{ private string text; private System.Collections.Generic.IList<string> words;
// Plain recursion: try every word that matches at the current position, and // solve the rest after it. A dead end is retried from scratch on every path that reaches it. public bool WordBreak(string text, System.Collections.Generic.IList<string> wordDict) { this.text = text; this.words = wordDict; return Can(0); }
private bool Can(int position) { // Reached the end: every letter is covered by a word. if (position == text.Length) return true; foreach (string word in words) { if (position + word.Length <= text.Length && string.CompareOrdinal(text, position, word, 0, word.Length) == 0 && Can(position + word.Length)) return true; } return false; }}// Plain recursion: try every word that matches at the current position, and// solve the rest after it. A dead end is retried from scratch on every path that reaches it.func wordBreak(text string, wordDict []string) bool { var can func(position int) bool can = func(position int) bool { // Reached the end: every letter is covered by a word. if position == len(text) { return true } for _, word := range wordDict { if strings.HasPrefix(text[position:], word) && can(position+len(word)) { return true } } return false } return can(0)}// Plain recursion: try every word that matches at the current position, and// solve the rest after it. A dead end is retried from scratch on every path that reaches it.function wordBreak(text: string, wordDict: string[]): boolean { const can = (position: number): boolean => { // Reached the end: every letter is covered by a word. if (position === text.length) return true; for (const word of wordDict) { if (text.startsWith(word, position) && can(position + word.length)) return true; } return false; }; return can(0);}1 Find the work that repeats
The work repeats through recursion. Each call loops over the m words, and checking one word compares up to k letters. So we count the calls, and the word checks inside each call.
- def can(position):
- if position == len(text): return True
- for word in words:
- if text starts with word at position:
- if can(position + len(word)): return True
- return False
- return can(0)
2 Count the runs on Example 1
The table has one column per call, in the order they ran. can(4) and can(6) checked all 4 words and found nothing that works. can(7) is the end, so it checks none.
5 calls and 2 + 4 + 4 + 3 + 0 = 13 word checks.
call can(0) can(4) can(6) can(3) can(7) words checked 2 4 4 3 0 total5 calls + 2 + 4 + 4 + 3 + 0 = 13 checks3 Write the count for any n
Each call to
can(position)comes from one way of cutting the first i letters into words. A string of i letters has i − 1 gaps, and each gap is a cut or not, so there are at most 2^(i − 1) ways. Over all i, that is fewer than 2ⁿ calls, and each does at most m checks of k letters:2ⁿ × m × k stepsCheck it with Example 1: 2⁷ = 128 calls at most. The real run made 5, because few words match. A string like “aaaaaaaaab” with the words “a”, “aa” and “aaa” really does try a huge number of cuts before failing.
4 Keep only the part that grows fastest
Nothing to drop: it is exponential in n. With n = 300, far too slow.
So the time is O(2ⁿ × m × k).
S Space: count the extra memory the same way
The call stack holds one chain of calls, at most n + 1 deep. So the space is O(n).
call stack (up to n deep)
Table of prefixes O(n × m × k)
The idea
Section titled “The idea”The recursion asks about the same positions many times. There are only n + 1 of them, so decide each once, in a table.
Let splittable[i] be true when the first i letters split into words. splittable[0] is true: no letters, no words.
For i from 1 to n, look for a word that could be the last word of the prefix: the prefix ends with
word, and splittable[i - len(word)] is true. If one exists, splittable[i] is true. The answer is splittable[n].
Why it is correct: a prefix splits exactly when it ends with some word and the part before that word splits. Shorter prefixes are filled in first, so their answers are final when they are read.
Walkthrough
Section titled “Walkthrough”- splittable = [True] + [False] * len(text)
- for i in 1 .. len(text):
- for word in words:
- if splittable[i - len(word)] and text[i - len(word):i] == word:
- splittable[i] = True; break
- return splittable[len(text)]
splittable[i] says whether the first i letters split into words. splittable[0] is true: zero letters need zero words. Fill the rest from left to right.
class Solution { // Table, bottom up: splittable[i] says whether the first i letters split into words. // Each prefix is decided once, from shorter prefixes. O(n × m × k) time, O(n) space. public boolean wordBreak(String text, List<String> wordDict) { boolean[] splittable = new boolean[text.length() + 1]; // The empty prefix needs no words. splittable[0] = true; for (int i = 1; i <= text.length(); i++) { for (String word : wordDict) { // w can be the last word if the letters before it already split. if (word.length() <= i && splittable[i - word.length()] && text.startsWith(word, i - word.length())) { splittable[i] = true; break; } } } return splittable[text.length()]; }}# Table, bottom up: splittable[i] says whether the first i letters split into words.# Each prefix is decided once, from shorter prefixes. O(n × m × k) time, O(n) space.def word_break(text: str, word_dict: list[str]) -> bool: splittable = [False] * (len(text) + 1) # The empty prefix needs no words. splittable[0] = True for i in range(1, len(text) + 1): for word in word_dict: # w can be the last word if the letters before it already split. if (len(word) <= i and splittable[i - len(word)] and text.startswith(word, i - len(word))): splittable[i] = True break return splittable[len(text)]public class Solution{ // Table, bottom up: splittable[i] says whether the first i letters split into words. // Each prefix is decided once, from shorter prefixes. O(n × m × k) time, O(n) space. public bool WordBreak(string text, System.Collections.Generic.IList<string> wordDict) { var splittable = new bool[text.Length + 1]; // The empty prefix needs no words. splittable[0] = true; for (int i = 1; i <= text.Length; i++) { foreach (string word in wordDict) { // w can be the last word if the letters before it already split. if (word.Length <= i && splittable[i - word.Length] && string.CompareOrdinal(text, i - word.Length, word, 0, word.Length) == 0) { splittable[i] = true; break; } } } return splittable[text.Length]; }}// Table, bottom up: splittable[i] says whether the first i letters split into words.// Each prefix is decided once, from shorter prefixes. O(n × m × k) time, O(n) space.func wordBreak(text string, wordDict []string) bool { splittable := make([]bool, len(text)+1) // The empty prefix needs no words. splittable[0] = true for i := 1; i <= len(text); i++ { for _, word := range wordDict { // w can be the last word if the letters before it already split. if len(word) <= i && splittable[i-len(word)] && strings.HasPrefix(text[i-len(word):], word) { splittable[i] = true break } } } return splittable[len(text)]}// Table, bottom up: splittable[i] says whether the first i letters split into words.// Each prefix is decided once, from shorter prefixes. O(n × m × k) time, O(n) space.function wordBreak(text: string, wordDict: string[]): boolean { const splittable = new Array<boolean>(text.length + 1).fill(false); // The empty prefix needs no words. splittable[0] = true; for (let i = 1; i <= text.length; i++) { for (const word of wordDict) { // w can be the last word if the letters before it already split. if ( word.length <= i && splittable[i - word.length] && text.startsWith(word, i - word.length) ) { splittable[i] = true; break; } } } return splittable[text.length];}1 Find the work that repeats
The inner loop checks one word against the end of the prefix. That compares up to k letters, so each word check is up to k steps.
- splittable = [True] + [False] * len(text)
- for i in 1 .. len(text):
- for word in words:
- if splittable[i - len(word)] and text[i - len(word):i] == word:
- splittable[i] = True; break
- return splittable[len(text)]
2 Count the runs on Example 1
The table has one column per prefix length. A prefix stops checking at the first word that works: “carp” at length 4 needed only 1 check.
4 + 4 + 2 + 1 + 4 + 4 + 3 = 22 word checks.
i 1 2 3 4 5 6 7 words checked 4 4 2 1 4 4 3 total4 + 4 + 2 + 1 + 4 + 4 + 3 = 22 checks3 Write the count for any n
The outer loop runs n times, the inner loop at most m times, and each check reads at most k letters:
n × m × k stepsCheck it with Example 1: 7 × 4 = 28 word checks at most. The real run made 22.
4 Keep only the part that grows fastest
Nothing to drop. It is O(n × m × k): at most 300 × 1000 × 20 = 6 million steps.
So the time is O(n × m × k).
S Space: count the extra memory the same way
The table has n + 1 slots. So the space is O(n).
splittable table (n + 1)
Tested at the limits
Section titled “Tested at the limits”2,000 random inputs: words of 1 to 3 letters made of a and b, and a text of up to 12 letters mostly built from those words, sometimes with one letter changed. The plain recursion and the table of prefixes gave the same answer every time.
| Largest inputs | Result |
|---|---|
| 300 letters and 1,000 words of up to 20 letters (the most allowed): 299 a then a b, with the words of only a at the end of the list. Table of prefixes. | 0.02 s, and about 0.002 MB of extra memory: one list of 301 flags, O(n). |
| 21 a then a b, with words of 1 to 5 a. Plain recursion. | 0.3 s. It tries every way to split the a again and again before it fails. At 300 letters it would never finish. |
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.