Start learning free

LESSON 455 OF 541 · DYNAMIC PROGRAMMING

Word Break in Swift: Dynamic Programming over Prefixes

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 10
    Ctrl + Enter to check

    SWIFT · DYNAMIC PROGRAMMING

    Word Break in Swift, explained

    Problem overview

    The method wordBreak receives a string s and an array of strings wordDict. It returns true when s can be cut into one or more dictionary words, laid one after another with no characters left over, and false otherwise. A word may be used as many times as you like.

    With s = "leetcode" and wordDict = ["leet", "code"] the answer is true: "leet" followed by "code". With "applepenapple" and ["apple", "pen"] it is also true, because "apple" is used twice. With "catsandog" and ["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: ask the question for every prefix

    Trying every way to cut the string is far too slow. With s = "aaaaaaab" and the words "a", "aa" and "aaa", the number of ways to cut the a's grows exponentially as the string grows, and none of them works, because nothing matches the final b.

    The way out is to solve smaller versions of the same problem. Let dp[i] be true when the first i characters of s can be split into words. The empty prefix needs no words, so dp[0] is true, and the answer to the whole problem is dp[n].

    To decide dp[i], look at what the last word of the split could be. If a word w with L characters ends the prefix, then the prefix is splittable when the part before that word, the first i - L characters, is splittable: dp[i - L] is true. So for each i you try every word. One word that fits is enough.

    The table for "leetcode" with ["leet", "code"], entries dp[0] to dp[8]:

    T F F F T F F F T

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

    T F F T T F F T F F

    The prefixes of lengths 3, 4 and 7 split ("cat", "cats", "catsand"), but the last two entries stay false, so the answer is false.

    Step by step

    1. Copy the characters into an array. A Swift String can't be indexed with an integer, so Array(s) gives something you can read by position and cut into pieces.
    2. Store the count in n. The table has one entry per prefix length, so this number comes up often.
    3. Create dp as n + 1 values, all false. It is a var because the loops write into it. A prefix counts as splittable only after a split is found.
    4. Set dp[0] to true. Every split starts from the empty prefix.
    5. Loop i over 1..<n + 1. The range stops before n + 1, so n, the whole string, is the last length. Each entry needs only shorter prefixes, which are already filled in.
    6. Loop over wordDict. Each word is a candidate for the last word of a split of the first i characters.
    7. Compute start as i - word.count, the index where the word would begin if it ended the prefix. It is negative when the word is longer than the prefix.
    8. Check three things with &&: the word fits (start >= 0), the part before it splits (dp[start]), and the prefix really ends with the word (String(chars[start..<i]) == word).
    9. Mark dp[i] as true inside the if.
    10. Return dp[n] after the outer loop.

    The solution in Swift

    class Solution {
        func wordBreak(_ s: String, _ wordDict: [String]) -> Bool {
            let chars = Array(s)
            let n = chars.count
            var dp = [Bool](repeating: false, count: n + 1)
            dp[0] = true
            for i in 1..<n + 1 {
                for word in wordDict {
                    let start = i - word.count
                    if start >= 0 && dp[start] && String(chars[start..<i]) == word {
                        dp[i] = true
                    }
                }
            }
            return dp[n]
        }
    }
    

    The Swift details worth a closer look:

    • Array of characters. Array(s) is an [Character], so chars[1] works where s[1] would not compile. chars.count and s.count agree, since both count characters.
    • Slicing. chars[start..<i] is an ArraySlice<Character>, not a string. String(...) builds a string from it, and == compares the contents of two strings.
    • Order of the checks. && stops at the first false. When start is negative, dp[start] is never read. Swift traps on an out-of-range index and the program crashes, so swapping the first two checks would crash the moment a word is longer than the prefix. The text comparison also runs only when the part before the word is splittable.
    • Range precedence. In 1..<n + 1, the + is applied first, so the range is 1..<(n + 1). It could also be written 1...n. Both are fine because s is never empty here; with an empty string n is 0 and 1...0 crashes at run time, since the lower bound is larger than the upper one.
    • Value types. dp is an array, which is a value type. Assigning dp[i] = true changes the array in place because dp is a var.

    Complexity

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

    Space: O(n). The table holds n + 1 booleans, and the character array holds n characters. The temporary strings are at most k characters long.

    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.

    Edge cases and common mistakes

    • Leftover characters. A split must use the whole string. "catsandog" has plenty of matching words but the answer is false because "og" is left over.
    • Reusing words. "applepenapple" needs "apple" twice. The loop never removes a word from consideration, so reuse is automatic.
    • Words that overlap in meaning. "abcd" with ["abcd", "ab"] is true through the single word "abcd", even though "ab" leaves "cd". A greedy approach that always takes the shortest or longest match can fail; the table tries every word at every position.
    • No word fits. "a" with ["b"] gives false: dp[1] is never set.
    • Reading before checking. Putting dp[start] before start >= 0 reads index -1 and crashes the program.

    Testing

    • "leetcode", ["leet", "code"] gives true.
    • "applepenapple", ["apple", "pen"] gives true.
    • "catsandog", ["cats", "dog", "sand", "and", "cat"] gives false.
    • "a", ["a"] gives true.
    • "a", ["b"] gives false.
    • "aaaaaaab", ["a", "aa", "aaa"] gives false.
    • "abcd", ["abcd", "ab"] gives true.
    • "cars", ["car", "ca", "rs"] gives true.
    Keep going with Swift500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free