Start learning free

LESSON 450 OF 536 · DYNAMIC PROGRAMMING

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

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

You are given a string s and a list 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

    PYTHON · DYNAMIC PROGRAMMING

    Word Break in Python, explained

    Problem overview

    The method wordBreak receives a string s and a list of words 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 can be used as many times as needed.

    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, and "apple" is used 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 statement 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 of cutting the string. That is hopeless in the worst case. With s made of many "a" characters followed by a "b", and the words "a", "aa", "aaa" and so on, the number of ways to cut the a's grows exponentially, and none of them works because of the final "b".

    The way out is to notice that the same smaller questions come up again and again. 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.
    • dp[i] is True when some word w ends the prefix, and the part before w can itself be split. In symbols: the word fits, s[i - len(w):i] == w, and dp[i - len(w)] is True.
    • The answer is dp[n], the whole string.

    Each entry depends only on entries to its left, so filling the table from left to right works. This is dynamic programming: each prefix is solved once, from the answers for shorter prefixes.

    For "leetcode" with ["leet", "code"] the finished table, indexed from 0 to 8, is:

    [True, False, False, False, True, False, False, False, True]
    

    Position 4 is True because "leet" ends there and position 0 is True. Position 8 is True because "code" ends there and position 4 is True. For "catsandog" with ["cats", "dog", "sand", "and", "cat"] the table is:

    [True, False, False, True, True, False, False, True, False, False]
    

    Positions 3, 4 and 7 are reachable ("cat", "cats" and the "and" or "sand" that follow), but the last entry stays False.

    Step by step

    1. Store the length of s in n. It is needed for the table and for the loop.
    2. Create the table dp with n + 1 entries, all False. There is one entry for every prefix length from 0 to n, and a prefix counts as splittable only once a split is found.
    3. Set dp[0] to True. Every split starts from the empty prefix.
    4. Loop over i in range(1, n + 1). range stops before its end, so the last value is n, the whole string.
    5. Loop over the words. Each one is tried as the last word of a split of the first i characters.
    6. Compute start, i - len(word): where the word would begin if it ended the prefix. It is negative when the word is longer than the prefix.
    7. Check three things with and: 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 dp[i] as True inside the if. One word that works is enough.
    9. Return dp[n] after both loops.

    The solution in Python

    class Solution:
        def wordBreak(self, s: str, wordDict: List[str]) -> bool:
            n = len(s)
            dp = [False] * (n + 1)
            dp[0] = True
            for i in range(1, n + 1):
                for word in wordDict:
                    start = i - len(word)
                    if start >= 0 and dp[start] and s[start:i] == word:
                        dp[i] = True
            return dp[n]
    

    Python details worth a closer look:

    • The order of the checks matters. and stops at the first check that is false. If start is negative, dp[start] is never read. That matters because Python accepts negative list indexes: with dp = [True, False], dp[-1] is False, the last element, and no error is raised. Without the guard, a long word would quietly read the wrong entry. Putting dp[start] before the string comparison also skips the comparison whenever the prefix before the word can't be split.
    • Slices are forgiving, indexes are not. s[start:i] holds the characters from index start up to i - 1, so it has exactly len(word) characters when start >= 0.
    • [False] * (n + 1) creates a list of n + 1 separate booleans. Repeating an immutable value like this is safe. (Repeating a list inside a list, as in [[]] * 3, would share one inner list.)
    • The loop keeps going after dp[i] is set. The solution doesn't break; setting an entry to True again changes nothing. Adding break after the assignment is a valid small speed-up.

    Complexity

    Time: O(n · m · k). The outer loop runs n times, the inner loop runs m times for each, and comparing a word with the end of the prefix takes up to k character comparisons.

    Space: O(n). The table has n + 1 entries. The slice s[start:i] is a temporary string of at most k characters.

    When the dictionary is large, a variation puts the words in a set and, for each i, looks up only the pieces of length 1 to k that end at i. That replaces the loop over m words with a loop over at most k lengths.

    Edge cases and common mistakes

    • Forgetting dp[0] = True. Then no entry can ever become True, because every first word of a split relies on it. The function always returns False.
    • Dropping the start >= 0 check. Negative indexes read from the end of the list instead of failing, which produces wrong answers silently.
    • Checking only that the word appears in s. The word has to end exactly at position i, and the part before it has to be splittable. "ba" with ["ab"] contains "ab" but isn't splittable; the function returns False.
    • Greedy matching. Taking the first word that fits can fail where another split works. The table tries every word at every position, so "aaaa" with ["a", "aa"] works regardless of order.
    • Returning dp[n - 1]. Index n stands for the first n characters, which is the whole string.

    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 = "ab", wordDict = ["a", "b"] gives True.
    • s = "ba", wordDict = ["ab"] gives False.
    • s = "aaaa", wordDict = ["a", "aa"] gives True.
    Keep going with Python500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free