150NeetCode 150

108.Word Break

MediumBlind 751-D Dynamic Programming Ā· 10/12

Problem

Given a string s and a dictionary wordDict, return true if s can be split into a sequence of one or more dictionary words with nothing left over.

Words may be reused any number of times, and you do not need to use every word.

Examples

Input:  s = "leetcode", wordDict = ["leet", "code"]
Output: true
Explanation: "leet" + "code".
Input:  s = "applepenapple", wordDict = ["apple", "pen"]
Output: true
Explanation: "apple" + "pen" + "apple" — "apple" is reused.
Input:  s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
Output: false
Explanation: "cats"+"and" or "cat"+"sand" both leave "og", which is not a word.

Constraints

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • s and wordDict[i] consist of lowercase English letters.
  • All strings in wordDict are unique.

šŸ’” Key Insight

Whether the suffix s[i..] can be segmented doesn't depend on how the prefix was split. So define f(i) = "can s[i..] be segmented?": f(n) = true, and f(i) = true if some word w matches at position i and f(i + |w|) is true. That's only n + 1 states. Filling them right-to-left gives O(n Ā· m Ā· L) with m words of length up to L; walking a Trie from each i checks all words at once, cutting it to O(n Ā· L).

Recursion (Brute Force)

TimeO(2ⁿ) in the worst caseSpaceO(n)

Intuition

At position i, try every dictionary word that matches s starting at i. If one of them leads to a successful segmentation of the rest, we're done.

Recurrence: f(i) = OR over w in wordDict with s.startsWith(w, i) of f(i + |w|), with f(n) = true.

Algorithm

  1. dfs(i): if i == n return true.
  2. For each w in wordDict: if s.startsWith(w, i) and dfs(i + w.length()), return true.
  3. Return false.

Visualization

s = "catsandog", dict [cats, dog, sand, and, cat]. Two different splits arrive at the same failing suffix f(7) 'og'.

flowchart TD
    A["f(0) 'catsandog'"] -->|"cat"| B["f(3) 'sandog'"]
    A -->|"cats"| C["f(4) 'andog'"]
    B -->|"sand"| D["f(7) 'og'"]
    C -->|"and"| E["f(7) 'og'"]
    D --> F["no word matches: false"]
    E --> G["no word matches: false"]

For longer inputs like "aaaa...ab" with dict [a, aa, aaa], the number of paths into the same suffix explodes exponentially.

Code

java
import java.util.List;

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        return dfs(s, 0, wordDict);
    }

    // can s[i..] be split into dictionary words?
    private boolean dfs(String s, int i, List<String> wordDict) {
        if (i == s.length()) {
            return true;
        }
        for (String w : wordDict) {
            if (s.startsWith(w, i) && dfs(s, i + w.length(), wordDict)) {
                return true;
            }
        }
        return false;
    }
}

Complexity

  • Time: O(2ⁿ) in the worst case — every split point can branch.
  • Space: O(n) — recursion depth.

Complexity Comparison

Approach Time Space
Recursion (Brute Force) O(2ⁿ) O(n)
Top-Down Memoization O(n Ā· m Ā· L) O(n)
Bottom-Up Tabulation O(n Ā· m Ā· L) O(n)
Bottom-Up DP with a Trie O(n · L + ΣL) O(n + ΣL)

Common Pitfalls

  • Greedy matching (always take the longest or first word) — "catsandog"-style inputs need backtracking.
  • Using a boolean[] memo without a "not computed" state: false is a real answer, so use Boolean[] or a separate visited array.
  • Forgetting dp[n] = true — without the empty-suffix base case nothing can ever be true.
  • Building substrings with s.substring(i, j) in a double loop creates O(n²) strings; startsWith(w, i) or a Trie walk avoids that.