Start learning free

LESSON 454 OF 540 · DYNAMIC PROGRAMMING

Word Break in Go: Dynamic Programming in O(n · m · k)

Practice O(n · m · k) Read the full explanation

You are given a string s and an array of strings wordDict, the dictionary. Return true if s can be split into one or more dictionary words, written one after another with nothing left over, or false otherwise.

The same dictionary word may be used any number of times in a split.

 

Example 1:

Input: s = "leetcode", wordDict = ["leet", "code"]
Output: true
Explanation: "leetcode" is "leet" followed by "code".

Example 2:

Input: s = "applepenapple", wordDict = ["apple", "pen"]
Output: true
Explanation: "applepenapple" is "apple" + "pen" + "apple". The word "apple" is used twice.

Example 3:

Input: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
Output: false
Explanation: A split can start with "cat" + "sand" or with "cats" + "and". Both leave "og",
which is not a word and doesn't start with one.

 

Constraints:

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • s and every wordDict[i] are made of lowercase English letters only, a to z.
  • All the words of wordDict are distinct.

Note:

Your algorithm should run in O(n · m · k) time and use O(n) extra space, where n is the length of s, m the number of words in wordDict and k the length of the longest word.



Trying every way to cut s is far too slow: with s = "aaa…ab" and the words "a", "aa", "aaa"…, the number of ways to cut the a's grows exponentially, and none of them works. Solve smaller versions of the problem instead: let dp[i] be true when the first i characters of s can be split into words. dp[0] is true: the empty prefix needs no words at all. The answer is dp[n].

Fill dp from left to right. The first i characters can be split exactly when they end with some word w and the part before that word can be split. If w has L characters, it must fit (L <= i), the L characters that end the prefix, from position i - L on, must spell w, and dp[i - L] must be true. Try every word for every i: one that works is enough.

The table has n + 1 entries. Each one tries the m words, and comparing a word with the end of the prefix takes up to k character comparisons: O(n · m · k) time. The table takes O(n) space.
When the dictionary is large, you can put the words in a map used as a set instead and, for each i, look up only the pieces of length 1 to k that end at i.
STEP 1 OF 9
    Ctrl + Enter to check

    GO · DYNAMIC PROGRAMMING

    Word Break in Go, explained

    Problem overview

    The function wordBreak gets a string s and a slice of strings wordDict. It returns true when s can be cut into one or more dictionary words laid end to end with nothing left over, and false otherwise. A word may be used as many times as needed.

    With s = "leetcode" and wordDict = ["leet", "code"] the answer is true: "leet" followed by "code". With s = "applepenapple" and the words "apple" and "pen" it is also true, and "apple" is used twice. With s = "catsandog" and the words "cats", "dog", "sand", "and" and "cat" it is false: both "cat" + "sand" and "cats" + "and" leave "og", which is no word.

    The lesson asks for O(n · m · k) time and O(n) extra space, where n is the length of s, m the number of words and k the length of the longest word.

    Approach: solve every prefix once

    The direct idea is to try every way to cut the string. That is hopeless: for s made of many as followed by a b, with the words "a", "aa", "aaa" and so on, the number of ways to cut the as grows exponentially, and none of them works because of the final b.

    The way out is to notice that the same questions repeat. Let dp[i] be true when the first i characters of s can be split into words. Then:

    • dp[0] is true. The empty prefix needs no words, and every split starts from it.
    • dp[i] is true when some word w ends the prefix and the part in front of it can be split. If w has L characters, that means L <= i, the characters from position i - L up to i spell w, and dp[i - L] is true.

    The answer is dp[n]. Each entry only looks at shorter prefixes, which are already done when the loop reaches it, so one pass from left to right is enough.

    Step by step

    1. Store the length. n := len(s) is used for the table size, the loop bound and the final answer.
    2. Create the table. make([]bool, n+1) has one entry per prefix length from 0 to n. A new slice of bools is filled with false, which is the right start: a prefix counts only once a split is found.
    3. Set dp[0] = true. This is the base case the first word of every split builds on.
    4. Loop over prefix lengths from 1 to n inclusive. The bound is i <= n because dp[n] is the entry we want.
    5. Try each word as the last word of the prefix, with for _, word := range wordDict. The index is not needed, so _ discards it.
    6. Compute where the word would start: start := i - len(word). The prefix ends at index i - 1, so a word of len(word) characters that ends it begins at i - len(word). That value is negative when the word is longer than the prefix.
    7. Check three things in one if, joined by &&: the word fits (start >= 0), the part before it can be split (dp[start]), and the prefix really ends with the word (s[start:i] == word).
    8. Mark the entry. One word that works is enough, so dp[i] = true.
    9. Return dp[n] after the loop.

    The solution in Go

    func wordBreak(s string, wordDict []string) bool {
        n := len(s)
        dp := make([]bool, n+1)
        dp[0] = true
        for i := 1; i <= n; i++ {
            for _, word := range wordDict {
                start := i - len(word)
                if start >= 0 && dp[start] && s[start:i] == word {
                    dp[i] = true
                }
            }
        }
        return dp[n]
    }
    

    The Go details worth a closer look:

    • The order of the checks matters. && stops at the first false. Because start >= 0 comes first, dp[start] is never read with a negative index. Without that check, wordBreak("leetcode", []string{"leet", "code"}) stops with the panic runtime error: index out of range [-3], since at i = 1 the word "leet" gives start = -3. Putting dp[start] before the comparison also means the text is compared only when the front part already splits.
    • s[start:i] is a substring. Slicing a string takes the bytes from start up to, but not including, i. It does not copy them. == compares two strings by their contents.
    • Bytes and letters. len(word) counts bytes. The lesson's strings hold only lowercase letters a to z, one byte each, so bytes and letters are the same thing here.
    • make([]bool, n+1) gives n+1 entries, all false. Indexing dp[n] is valid because the last index is n.

    Complexity

    Time: O(n · m · k). The table has n + 1 entries. For each one the code tries m words, and comparing a word with the end of the prefix takes up to k character comparisons.

    Space: O(n). The table is the only structure that grows with the input.

    When the dictionary is large, you can keep the words in a map[string]bool and, for each i, look up only the pieces of length 1 to k that end at i. That replaces the loop over m words by at most k lookups.

    Edge cases and common mistakes

    • Leaving out dp[0] = true. Then no entry can ever become true, and the function always returns false.
    • Checking only the start of the string. A greedy rule such as "take the first word that matches" fails: for s = "cars" and the words "car", "ca" and "rs", taking "car" leaves "s", yet "ca" + "rs" works. The function returns true because dp[4] is reached through dp[2].
    • A prefix that works but leaves a tail. For s = "catsandog", dp[7] is true, but dp[9] is not, because nothing ends at the final g from a splittable position. Only dp[n] answers the question.
    • A word longer than the string. wordBreak("a", []string{"b"}) returns false, and a longer word such as "abc" is skipped through the start >= 0 check.

    Testing

    Here is the table for s = "leetcode" with ["leet", "code"], entries dp[0] to dp[8]:

    [true false false false true false false false true]

    dp[4] is true because "leet" ends the prefix and dp[0] is true. dp[8] is true because "code" ends the string and dp[4] is true. For "catsandog" the table is:

    [true false false true true false false true false false]

    The last entry is false, so the answer is false. More inputs to try:

    • s = "applepenapple", words ["apple", "pen"] gives true.
    • s = "abcd", words ["a", "abc", "b", "cd"] gives true ("a" + "b" + "cd").
    • s = "cars", words ["car", "ca", "rs"] gives true.
    • s = "a", words ["b"] gives false.
    • s made of 29 as followed by b, words ["a", "aa", "aaa", "aaaa"] gives false, quickly.
    Keep going with Go500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free