Start learning free

LESSON 450 OF 536 · DYNAMIC PROGRAMMING

Word Break in JavaScript: 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 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

    JAVASCRIPT · DYNAMIC PROGRAMMING

    Word Break in JavaScript, explained

    Problem overview

    The method wordBreak(s, wordDict) receives a string s and an array of words. It returns true when s can be cut into pieces that are all dictionary words, with nothing left over, and false otherwise. A word can be used as many times as you like.

    For s = "leetcode" and wordDict = ["leet", "code"] the answer is true: the string is "leet" followed by "code". For "applepenapple" with ["apple", "pen"] it is also true, using "apple" twice. For "catsandog" with ["cats", "dog", "sand", "and", "cat"] it is false: both "cat" + "sand" and "cats" + "and" leave "og", which is not a 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: answer the question for every prefix

    Trying every way to cut the string is far too slow. With s = "aaa…ab" and the words "a", "aa", "aaa", the a's can be cut in exponentially many ways, and none of them works because of the final b. A search that explores each way separately repeats the same work again and again.

    Dynamic programming avoids that by solving smaller versions of the problem first. Let dp[i] be true when the first i characters of s can be split into words. Two facts define the whole table:

    • dp[0] is true. The empty prefix needs no words, and every split has to start from it.
    • dp[i] is true when some word w ends the prefix and the part before w can be split. If w has L characters, that means the prefix is at least that long, its last L characters spell w, and dp[i - L] is true.

    The answer is dp[n], the entry for the whole string. For "leetcode" the table, read from index 0 to 8, comes out as:

    0:true 1:false 2:false 3:false 4:true 5:false 6:false 7:false 8:true

    Index 4 is true because "leet" ends there and dp[0] is true. Index 8 is true because "code" ends there and dp[4] is true.

    Step by step

    1. Store the length. n = s.length is used for the table size, the loop bound and the final answer.
    2. Create the table. new Array(n + 1).fill(false) has one entry for each prefix length from 0 to n. Everything starts as false: a prefix counts only once a split has been found for it.
    3. Set dp[0] to true. This is the base case that every other true grows from.
    4. Loop over prefix lengths from 1 up to and including n. Each entry needs only shorter prefixes, which are already filled in by the time the loop reaches it.
    5. Try each word as the last word of the prefix with a for...of loop over wordDict.
    6. Compute where the word would start. A prefix of i characters ends at index i - 1, so a word of word.length characters ends it by starting at i - word.length. That number is negative when the word is longer than the prefix.
    7. Check three things together. The word fits (start >= 0), the part before it can be split (dp[start]), and the prefix really ends with the word (s.substring(start, i) === word). The checks run left to right and stop at the first one that fails, so the text comparison happens only when the cheaper checks passed.
    8. Mark the entry. One word that works is enough, so dp[i] = true.
    9. Return dp[n].

    The solution in JavaScript

    class Solution {
        wordBreak(s, wordDict) {
            const n = s.length;
            const dp = new Array(n + 1).fill(false);
            dp[0] = true;
            for (let i = 1; i <= n; i++) {
                for (const word of wordDict) {
                    const start = i - word.length;
                    if (start >= 0 && dp[start] && s.substring(start, i) === word) {
                        dp[i] = true;
                    }
                }
            }
            return dp[n];
        }
    }
    

    A few JavaScript details matter here:

    • substring(start, i) excludes i. It returns the characters from index start up to i - 1, which is exactly word.length characters.
    • The start >= 0 check is not optional. substring treats a negative index as 0 instead of failing: "abc".substring(-1, 2) is "ab". Without the check, a word longer than the prefix would be compared against a piece that starts at the wrong place.
    • Use === for the comparison. Strings are compared by their characters, so s.substring(start, i) === word is true when the text matches, even though the two strings are different objects in memory.
    • fill(false) is safe here. The entries are booleans, which are copied by value. Filling an array of arrays this way would not be safe, because every row would be the same array.
    • for...of over an array gives the words themselves, not their indexes, which is why the loop variable is named word.

    Complexity

    Time: O(n · m · k). The table has n + 1 entries. Each one tries all 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. The pieces cut by substring are short and temporary.

    When the dictionary is large, you can put the words in a Set and, for each i, look up only the pieces of length 1 to k that end at i. That replaces the loop over all m words with at most k lookups.

    Edge cases and common mistakes

    • Forgetting dp[0] = true. Every entry stays false, and every call returns false.
    • Greedy matching. Taking the first word that matches at the front can fail. For "abcd" with ["ab", "abc", "cd"] the answer is true ("ab" + "cd"), but taking "abc" first leaves "d". The table considers every word at every position.
    • Reusing words. "abab" with ["ab"] is true: the table never marks a word as used up.
    • Returning the wrong entry. The result is dp[n], not the last entry that happens to be true. For "ab" with ["abc"] no word fits, and the answer is false.
    • Using i < n. The loop must include i = n, because that entry is the answer.

    Testing

    • s = "leetcode", wordDict = ["leet", "code"] gives true.
    • s = "applepenapple", wordDict = ["apple", "pen"] gives true.
    • s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] gives false.
    • s = "a", wordDict = ["b"] gives false.
    • s = "aaaaab", wordDict = ["a", "aa", "aaa"] gives false.
    • s = "abcd", wordDict = ["ab", "abc", "cd"] gives true.
    Keep going with JavaScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free