Start learning free

LESSON 457 OF 543 · DYNAMIC PROGRAMMING

Word Break in Rust: 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 HashSet instead and, for each i, look up only the pieces of length 1 to k that end at i.
STEP 1 OF 8
    Ctrl + Enter to check

    RUST · DYNAMIC PROGRAMMING

    Word Break in Rust, explained

    Problem overview

    The function word_break receives a string s and a vector of words, word_dict. It returns true when s can be cut into pieces that are all dictionary words, laid one after another with nothing left over. A word may be used as many times as you like.

    With s = "leetcode" and the words ["leet", "code"] the answer is true: the string is "leet" followed by "code". With s = "applepenapple" and ["apple", "pen"] it is also true, because "apple" is used twice. With s = "catsandog" and ["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: can each prefix be split?

    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, and none of them works because of the final b.

    The fix is to solve smaller versions of the problem. Let dp[i] be true when the first i characters of s can be split into words. The answer is dp[n], the whole string.

    How is dp[i] decided? If the first i characters can be split, the split has a last word. Call it word, with L characters. Then two things hold: the last L characters of the prefix spell word, and the part before them, the first i - L characters, can be split too. That second fact is already in the table, because i - L is smaller than i. So each entry is built from earlier ones, which is what makes this dynamic programming.

    The base case is dp[0] = true: the empty prefix needs no words. Every split starts from it.

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

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

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

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

    "cat" ends at 3, "cats" at 4, and "sand" and "and" both end at 7, but nothing ends at 9, so the last entry is false.

    Step by step

    1. Store the length. n = s.len() is used for the table size and the loop bound. The string holds only the letters a to z, one byte each, so its length in bytes is its number of characters.
    2. Create the table. vec![false; n + 1] has one entry for every prefix length from 0 to n. A prefix counts as splittable only once a split is found, so everything starts as false.
    3. Set dp[0] to true, the empty prefix.
    4. Loop over prefix lengths from 1 to n inclusive. The range 1..=n includes n, and every entry depends only on shorter prefixes, which are finished by then.
    5. Try each word as the last word of the current prefix, borrowing the vector with &word_dict so nothing is copied.
    6. Check three things, joined by &&: the word fits in the prefix, the part before it can be split, and the slice that ends the prefix equals the word.
    7. Mark dp[i] true when all three hold. One word that works is enough.
    8. Return dp[n] without a semicolon, as the function's final expression.

    The solution in Rust

    impl Solution {
        fn word_break(s: String, word_dict: Vec<String>) -> bool {
            let n = s.len();
            let mut dp = vec![false; n + 1];
            dp[0] = true;
            for i in 1..=n {
                for word in &word_dict {
                    if word.len() <= i && dp[i - word.len()] && &s[i - word.len()..i] == word {
                        dp[i] = true;
                    }
                }
            }
            dp[n]
        }
    }
    

    The Rust details worth a closer look:

    • The order of the three checks matters. Indexes are usize, which can't go below zero. If word.len() is 4 and i is 2, the subtraction i - word.len() underflows: it panics in a debug build and wraps to a huge number in an optimized build, and the indexing that follows would then panic. Because && stops at the first check that fails, word.len() <= i runs first and the subtraction only happens when it is safe.
    • Slicing a string. &s[a..b] is a &str holding the bytes from a up to, but not including, b. Slicing a String uses byte positions, not character positions. That works here because the letters are all one byte each. With text that has multi-byte characters, a cut in the middle of a character panics.
    • Comparing the slice with word. word is a &String, because the loop borrows the vector. The left side is a &str, and Rust can compare a &str with a &String by their text, so the comparison checks the characters, not addresses.
    • for word in &word_dict. Looping over the vector itself would move it into the loop, and it would be unusable for the next value of i. The & borrows it each time.
    • Final expression. dp[n] has no semicolon, so it is the value the function returns. A bool is Copy, so reading it out of the vector is fine.

    Complexity

    Time: O(n · m · k). 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, where k is the length of the longest word.

    Space: O(n). The only extra memory is the table of n + 1 booleans. When the dictionary is large, an alternative is to put the words in a HashSet and, for each i, look up only the pieces of length 1 to k that end at i.

    Edge cases and common mistakes

    • A word longer than the prefix. Skipping the length check turns i - word.len() into an underflow. With s = "a" and the word "b" nothing breaks, but any longer word in the dictionary would.
    • Greedy matching. Taking the longest word that fits at each point can fail. With s = "cars" and the words ["car", "ca", "rs"], grabbing "car" leaves "s", which is not a word, yet "ca" + "rs" works. The table finds it, because every prefix is judged independently.
    • Leftover characters. The words must cover the whole string. For s = "aaaaaaab" with ["a", "aa", "aaa"] the first seven characters can be split in many ways, but the answer is false, because no word ends at position 8.
    • Returning the wrong entry. The answer is dp[n], not dp[n - 1]: the table has n + 1 entries because it includes the empty prefix.
    • A word used more than once. Nothing in the loop marks a word as used, so repeated words work without extra code, as in "applepenapple".

    Testing

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