String · 17 of 43
Word break Problem
Check if a long string with no spaces can be cut into pieces that are all real words from a word list.
- Simple way
- time O(2^n)
room O(n) - Clever way
- time O(n * L^2)
room O(n + D)
The problem, simply
You get a string with no spaces, like catsanddog, and a list of words, the dictionary. Can you put spaces into the string so that every piece is a word from the dictionary? A word may be used many times.
Give back true or false. Below, n is the length of the string, L is the length of the longest dictionary word, and D is the total size of the dictionary.
A story to remember it
Picture it
catsanddog with the dictionary. The planks cat, sand and dog get you across. So do cats, and, dog.
- done
- answer
- path
Examples
Two words
- In
s = 'leetcode', dict = [leet, code]- Out
true
leet + code.
A word used twice
- In
s = 'applepenapple', dict = [apple, pen]- Out
true
apple + pen + apple. The same word may be used again.
Almost, but no
- In
s = 'catsandog', dict = [cats, dog, sand, and, cat]- Out
false
cats + and leaves og, and cat + sand leaves og. No split covers the end.
A word not in the list
- In
s = 'a', dict = [b]- Out
false
The only piece, a, is not a word. An empty string gives true, because zero pieces is a valid split.
Way 1: the simple way
Try every first word. If the start of the string is a dictionary word, ask the same question about the rest.
- If the string is empty, return
true. - For every length
i, take the firstiletters. - If they are a word and the rest can be split, return
true. - If no first word works, return
false.
The same rest gets asked about again and again. Here is the tree of questions for aaab with the words a and aa. Each number is where the rest starts.
Starting points 2 and 3 are asked about more than once. On longer inputs like aaaaaaaaab the repeats explode.
- checking
- rejected
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
return canBreak(s, new HashSet<>(wordDict));
}
private boolean canBreak(String s, Set<String> dict) {
if (s.isEmpty()) return true; // nothing left to split
for (int i = 1; i <= s.length(); i++) {
if (dict.contains(s.substring(0, i)) // first piece is a word
&& canBreak(s.substring(i), dict)) { // and the rest works too
return true;
}
}
return false;
}
}How long it takes: in the worst case every way of cutting is tried, and there are 2^(n - 1) ways, so O(2^n). Extra room: the call stack, up to n deep, O(n) (plus the substrings).
Way 2: the clever way
Answer each starting spot only once, from left to right. dp[i] says whether the first i letters can be split. Spot i sits between letter i - 1 and letter i.
dp[0] = true, the near bank.- For each
ifrom1ton, look back at spotsjfromi - Ltoi - 1. - If
dp[j]is true and the letters fromjtoiform a word, setdp[i] = trueand stop looking. - The answer is
dp[n], the far bank.
Looking back at most L letters is enough, because no word is longer than L.
Plant a flag on every spot you can reach
01 / 09
dp[0] is T: zero letters can always be split, into nothing. Every other box starts unknown.
- checking
- done
- answer
- rejected
- path
- just added
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> dict = new HashSet<>(wordDict);
int longest = 0;
for (String w : wordDict) longest = Math.max(longest, w.length());
int n = s.length();
boolean[] dp = new boolean[n + 1]; // dp[i]: first i letters can be split
dp[0] = true; // the near bank
for (int i = 1; i <= n; i++) {
for (int j = Math.max(0, i - longest); j < i; j++) {
if (dp[j] && dict.contains(s.substring(j, i))) { // stand at j, plank to i
dp[i] = true;
break;
}
}
}
return dp[n];
}
}How long it takes: n spots, up to L planks each, and each plank costs up to L to cut out and look up, so O(n * L^2). Without the longest limit it is O(n^3). Extra room: the dp array plus the word set, O(n + D).
Watch out for
- Off by one.
dphasn + 1boxes.dp[i]is about the firstiletters, and the piece iss.substring(j, i). - Searching the list instead of a set.
List.containschecks every word, which adds a factor ofD. Put the words in aHashSet. - Forgetting
dp[0] = true. Without it nothing can ever become true. - Memoizing by substring instead of by index. Use the start index as the key; it is cheaper and clearer.
What interviewers ask next
- Return all the sentences (LeetCode 140). Do the same search with memo by start index, but store the list of sentences for each start. The output itself can be exponential.
- A huge dictionary? Put the words in a trie. From each true spot, walk the trie forward letter by letter, and stop as soon as no word continues. This avoids building substrings.
- Fewest words in a split? Store a number in
dpinstead of true or false:dp[i] = min(dp[j] + 1). - Is it like coin change? Yes. The words are coins, the string is the amount, and the order of coins is fixed by the letters.
Remember this
Grown-up words: the spots you can stand on are a 1D DP table, the river crossing is a reachability question on the positions, the repeated rest of the string is overlapping subproblems, and the dictionary is a hash set.