Problem overview
You get a string s and an array of words wordDict. The function wordBreak returns true if s can be cut into pieces that are all dictionary words, with nothing left over. A word can be used as many times as you like.
For s = "leetcode" and the words "leet" and "code" the answer is true: the string is "leet" followed by "code". For "applepenapple" with "apple" and "pen" it is also true, because "apple" is used twice. For "catsandog" with "cats", "dog", "sand", "and" and "cat" it is false: both "cat" + "sand" and "cats" + "and" leave "og", which is not a word.
Approach
The direct idea is to try every way to cut the string. That is far too slow. With s = "aaaaaaab" and the words "a", "aa" and "aaa", the a's can be cut in exponentially many ways, and every one of them fails at the final b. A search that explores all of them repeats the same work over and over.
Dynamic programming removes the repetition. Let dp[i] be true when the first i characters of s can be split into words. Two facts define the table:
dp[0]istrue. The empty prefix needs no words, and every split starts from it.dp[i]istruewhen the prefix of lengthiends with some wordwand the part before that word, of lengthi - w.length, can be split too.
The answer is dp[n], where n is the length of s. Each entry only needs entries for shorter prefixes, so filling the table from left to right is enough.
Here is the table for "catsandog" from a real run. Entry 0 is the empty prefix, entry 9 the whole string:
T F F T T F F T F F
Entries 3, 4 and 7 are T: "cat", "cats" and "catsand" (which is "cat" + "sand" or "cats" + "and") can be split. Entry 9 is F, so the string can't. For "leetcode" the table is:
T F F F T F F F T
Entry 4 ("leet") is true, and entry 8 is true because it ends with "code" and entry 4 is true.
Step by step
- Store the length.
val n = s.length. It appears in the table size, the loop bound and the result. - Create the table.
BooleanArray(n + 1)has one entry per prefix length, from 0 ton. A newBooleanArrayis filled withfalse, which is the right starting value: a prefix counts only once a split is found. - Seed the empty prefix.
dp[0] = true. Without it nothing could ever become true. - Loop over prefix lengths.
for (i in 1..n)includesn, because the whole string is the last prefix. - Try each word as the last word. The inner loop
for (word in wordDict)asks whether this word could end the prefix of lengthi. - Find where the word would start. The prefix ends at index
i - 1, so a word ofword.lengthcharacters that ends it begins atstart = i - word.length. This is negative when the word is longer than the prefix. - Check three conditions.
start >= 0says the word fits.dp[start]says the part before it can be split.s.substring(start, i) == wordsays the prefix really ends with the word. They are joined with&&, which stops at the first false check, sodp[start]is never read with a negative index and the strings are compared only when the cheaper checks pass. - Record the success. Set
dp[i] = true. One working word is enough, and later words can't undo it. - Return the last entry.
dp[n]covers the whole string.
The solution in Kotlin
class Solution {
fun wordBreak(s: String, wordDict: Array<String>): Boolean {
val n = s.length
val dp = BooleanArray(n + 1)
dp[0] = true
for (i in 1..n) {
for (word in wordDict) {
val start = i - word.length
if (start >= 0 && dp[start] && s.substring(start, i) == word) {
dp[i] = true
}
}
}
return dp[n]
}
}
Some Kotlin details that matter here:
==compares contents. On strings, Kotlin's==callsequals, so it compares characters, not references.s.substring(start, i) == wordis true when the two texts match, even though they are different objects.substring(start, i)excludesi. It takes the characters from indexstartup toi - 1, exactlyword.lengthof them. With a negative start it would throw aStringIndexOutOfBoundsException, which is why thestart >= 0check comes first.1..nincludesn. Unlike0 until n, a range written with..has an inclusive end.- Arrays of strings. The dictionary is an
Array<String>, andfor (word in wordDict)walks over it directly.
Complexity
The table has n + 1 entries. Each entry tries all m words, and comparing one word with the end of the prefix takes up to k character comparisons, where k is the length of the longest word. That makes O(n · m · k) time. The table is the only extra storage, so the space is O(n). (substring also copies up to k characters, which stays within the same bound.)
When the dictionary is large, put the words in a HashSet and, for each i, look up only the pieces of length 1 to k that end at i. That replaces the loop over every word.
Edge cases and common mistakes
- Forgetting
dp[0] = true. Every entry staysfalseand the function always returnsfalse. - Using
0 until nfor the loop. The table would never fill entryn, and the function would return the defaultfalse. - Checking only that the end matches. Skipping
dp[start]accepts strings where a word matches at the end but the front can't be split. For"abcd"with"abc"and"cd", the word "cd" matches the end, yet "ab" is not splittable. - Stopping at the first word that matches the front. A greedy choice fails when a shorter word is the right one. In
"catsandog", picking "cats" or "cat" first both lead nowhere, and the table handles both at once. - Reading
dp[start]with a negative index. Putstart >= 0first in the condition. - A word that never fits. A word longer than
sgives a negativestartfor everyiand is skipped.
Testing
Inputs worth trying, with the results the solution gives:
"leetcode"with"leet","code":true"applepenapple"with"apple","pen":true"catsandog"with"cats","dog","sand","and","cat":false"a"with"b":false"aaaaaaab"with"a","aa","aaa":false, quickly, because the table never repeats work
