139. Word Break - Explanation

Problem Link

Description

Given a string s and a dictionary of strings wordDict, return true if s can be segmented into a space-separated sequence of dictionary words.

You are allowed to reuse words in the dictionary an unlimited number of times. You may assume all dictionary words are unique.

Example 1:

Input: s = "neetcode", wordDict = ["neet","code"]

Output: true

Explanation: Return true because "neetcode" can be split into "neet" and "code".

Example 2:

Input: s = "applepenapple", wordDict = ["apple","pen","ape"]

Output: true

Explanation: Return true because "applepenapple" can be split into "apple", "pen" and "apple". Notice that we can reuse words and also not use all the words.

Example 3:

Input: s = "catsincars", wordDict = ["cats","cat","sin","in","car"]

Output: false

Constraints:

  • 1 <= s.length <= 200
  • 1 <= wordDict.length <= 100
  • 1 <= wordDict[i].length <= 20
  • s and wordDict[i] consist of only lowercase English letters.


Topics

Recommended Time & Space Complexity

You should aim for a solution as good or better than O(n * m * t) time and O(n) space, where n is the length of the string s, m is the number of words in wordDict, and t is the maximum length of any word in wordDict.


Hint 1

Try to think of this problem in terms of recursion, where we explore all possibilities. We iterate through the given string s, attempting to pick a word from wordDict that matches a portion of s, and then recursively continue processing the remaining string. Can you determine the recurrence relation and base condition?


Hint 2

The base condition is to return true if we reach the end of the string s. At each recursive call with index i iterating through s, we check all words in wordDict and recursively process the remaining string by incrementing i by the length of the matched word. If any recursive path returns true, we immediately return true. However, this solution is exponential. Can you think of an optimization? Maybe you should consider an approach that avoids repeated work.


Hint 3

We can avoid recalculating results for recursive calls by using memoization. Since we iterate with index i, we can use a hash map or an array of the same length as s to cache the results of recursive calls and prevent redundant computations.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Recursion - Understanding how to break problems into smaller subproblems and handle base cases
  • Dynamic Programming - Both memoization (top-down) and tabulation (bottom-up) approaches
  • Hash Set - Using sets for O(1) lookup to efficiently check if a word exists in the dictionary
  • String Manipulation - Substring operations and string comparison
  • Trie (Optional) - A tree-based data structure for efficient prefix matching and word lookup

1. Recursion

Intuition

At every index i in the string, we want to decide:

Can the suffix starting at index i be segmented into valid dictionary words?

The recursive idea is:

  • Try every word in wordDict
  • If a word matches the string starting at position i
  • Recursively check whether the remaining substring (starting at i + len(word)) can also be broken successfully

If any path reaches the end of the string, the answer is true.

This is a classic decision-based recursion where:

  • Each index i represents a subproblem
  • Base case: reaching the end means a valid segmentation

Algorithm

  1. Define a recursive function dfs(i):
    • If i == len(s), return true
  2. For each word w in wordDict:
    • Check if w matches s[i : i + len(w)]
    • If it matches and dfs(i + len(w)) is true, return true
  3. If no word leads to a valid segmentation, return false
  4. Start recursion from index 0
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:

        def dfs(i):
            if i == len(s):
                return True

            for w in wordDict:
                if ((i + len(w)) <= len(s) and
                     s[i : i + len(w)] == w
                ):
                    if dfs(i + len(w)):
                        return True
            return False

        return dfs(0)

Time & Space Complexity

  • Time complexity: O(tmn)O(t * m ^ n)
  • Space complexity: O(n)O(n)

Where nn is the length of the string ss, mm is the number of words in wordDictwordDict and tt is the maximum length of any word in wordDictwordDict.


2. Recursion (Hash Set)

Intuition

This version improves the brute-force recursion by optimizing word lookup.

Instead of trying every word from wordDict at each index, we:

  • Fix a starting index i
  • Try all possible substrings s[i : j+1]
  • Check if the substring exists in a Hash Set (O(1) lookup)

If a valid word is found:

  • Recursively check whether the remaining suffix starting at j + 1 can be segmented

The key idea:

If we can split the string at any valid word boundary and the rest is solvable, then the whole string is solvable.

Algorithm

  1. Convert wordDict into a hash set wordSet for fast lookup
  2. Define a recursive function dfs(i):
    • If i == len(s), return true
  3. For every j from i to len(s) - 1:
    • If s[i : j + 1] is in wordSet
      • If dfs(j + 1) is true, return true
  4. If no split works, return false
  5. Start recursion from index 0
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        wordSet = set(wordDict)

        def dfs(i):
            if i == len(s):
                return True

            for j in range(i, len(s)):
                if s[i : j + 1] in wordSet:
                    if dfs(j + 1):
                        return True
            return False

        return dfs(0)

Time & Space Complexity

  • Time complexity: O((n2n)+m)O( (n * 2 ^ n) + m)
  • Space complexity: O(n+(mt))O(n + (m * t))

Where nn is the length of the string ss and mm is the number of words in wordDictwordDict.


3. Dynamic Programming (Top-Down)

Intuition

This is an optimized version of recursion using memoization.

The key observation:

  • While recursively checking splits, the same index i is reached many times
  • The result of dfs(i) (can suffix s[i:] be segmented?) never changes

So we cache the result for each index:

  • If dfs(i) was already computed, reuse it
  • This avoids recomputing exponential subtrees

In short:

Convert exponential recursion into linear states using memoization.

Algorithm

  1. Use a hash map memo where:
    • memo[i] = true/false means whether s[i:] can be segmented
    • Base case: memo[len(s)] = true
  2. Define dfs(i):
    • If i is in memo, return memo[i]
  3. For each word w in wordDict:
    • If s[i : i + len(w)] == w
      • Recursively call dfs(i + len(w))
      • If it returns true, store memo[i] = true and return true
  4. If no word leads to a valid split:
    • Store memo[i] = false
  5. Return dfs(0)
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        memo = {len(s) : True}
        def dfs(i):
            if i in memo:
                return memo[i]

            for w in wordDict:
                if ((i + len(w)) <= len(s) and
                     s[i : i + len(w)] == w
                ):
                    if dfs(i + len(w)):
                        memo[i] = True
                        return True
            memo[i] = False
            return False

        return dfs(0)

Time & Space Complexity

  • Time complexity: O(nmt)O(n * m * t)
  • Space complexity: O(n)O(n)

Where nn is the length of the string ss, mm is the number of words in wordDictwordDict and tt is the maximum length of any word in wordDictwordDict.


4. Dynamic Programming (Hash Set)

Intuition

This approach is a top-down dynamic programming solution with pruning.

Key ideas:

  • Checking every possible substring is expensive.
  • A word can only be as long as the maximum word length in wordDict.
  • Use a Hash Set for O(1) word lookup.
  • Use memoization so each index in the string is solved only once.

So we:

  • Limit how far we try to split from each index
  • Cache results for indices to avoid repeated work

This turns exponential recursion into efficient DP.

Algorithm

  1. Convert wordDict into a hash set wordSet
  2. Compute t = maximum length of any word in wordDict
  3. Use a memo map where memo[i] means:
    • Can substring s[i:] be segmented?
  4. Define dfs(i):
    • If i is in memo, return memo[i]
    • If i == len(s), return true
  5. For j from i to min(len(s), i + t) - 1:
    • If s[i : j + 1] is in wordSet
      • If dfs(j + 1) is true, store and return true
  6. If no valid split works:
    • Store memo[i] = false
  7. Return dfs(0)
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        wordSet = set(wordDict)
        t = 0
        for w in wordDict:
            t = max(t, len(w))

        memo = {}
        def dfs(i):
            if i in memo:
                return memo[i]
            if i == len(s):
                return True
            for j in range(i, min(len(s), i + t)):
                if s[i : j + 1] in wordSet:
                    if dfs(j + 1):
                        memo[i] = True
                        return True
            memo[i] = False
            return False

        return dfs(0)

Time & Space Complexity

  • Time complexity: O((t2n)+m)O((t ^ 2 * n) + m)
  • Space complexity: O(n+(mt))O(n + (m * t))

Where nn is the length of the string ss, mm is the number of words in wordDictwordDict and tt is the maximum length of any word in wordDictwordDict.


5. Dynamic Programming (Bottom-Up)

Intuition

This is a bottom-up dynamic programming approach.

Instead of trying to split the string recursively, we solve the problem from the end of the string toward the start.

Key idea:

  • dp[i] means whether the substring s[i:] can be segmented
  • If we know the answer for future positions, we can decide the current one
  • We reuse already computed results → no recursion, no stack overhead

Algorithm

  1. Create a boolean array dp of size len(s) + 1
    • dp[i] = true if s[i:] can be segmented
  2. Base case:
    • dp[len(s)] = true (empty string is valid)
  3. Iterate i from len(s) - 1 down to 0:
    • For each word w in wordDict:
      • If s[i : i + len(w)] == w
        • Set dp[i] = dp[i + len(w)]
      • If dp[i] becomes true, break early
  4. Return dp[0]
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        dp = [False] * (len(s) + 1)
        dp[len(s)] = True

        for i in range(len(s) - 1, -1, -1):
            for w in wordDict:
                if (i + len(w)) <= len(s) and s[i : i + len(w)] == w:
                    dp[i] = dp[i + len(w)]
                if dp[i]:
                    break

        return dp[0]

Time & Space Complexity

  • Time complexity: O(nmt)O(n * m * t)
  • Space complexity: O(n)O(n)

Where nn is the length of the string ss, mm is the number of words in wordDictwordDict and tt is the maximum length of any word in wordDictwordDict.


6. Dynamic Programming (Trie)

Intuition

The normal DP checks every word at every index, which can waste time comparing strings again and again.

A Trie stores all dictionary words like a prefix tree, so from any starting index i in s, we can walk forward character-by-character and quickly know:

  • whether the current prefix matches some dictionary word path
  • and when we hit a complete word (is_word = true)

We still use DP:

  • dp[i] = can we break the suffix s[i:] into dictionary words?
  • If from i we can reach some j where s[i..j] is a word, then dp[i] = dp[j+1]

Trie helps us find valid words starting at i efficiently.

Algorithm

  1. Build a Trie from all words in wordDict.
  2. Create a boolean DP array dp of size n + 1 where n = len(s).
    • dp[n] = true (empty suffix is always valid)
  3. Let t be the maximum word length in the dictionary (use it as a bound).
  4. Fill DP from right to left:
    • For each index i from n down to 0:
      • Try all end positions j from i to min(n-1, i+t-1):
        • If s[i..j] is a word in the Trie, set dp[i] = dp[j+1]
        • If dp[i] becomes true, stop early for this i
  5. Return dp[0].
class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_word = False

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
        node = self.root
        for char in word:
            if char not in node.children:
                node.children[char] = TrieNode()
            node = node.children[char]
        node.is_word = True

    def search(self, s, i, j):
        node = self.root
        for idx in range(i, j + 1):
            if s[idx] not in node.children:
                return False
            node = node.children[s[idx]]
        return node.is_word

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        trie = Trie()
        for word in wordDict:
            trie.insert(word)

        dp = [False] * (len(s) + 1)
        dp[len(s)] = True

        t = 0
        for w in wordDict:
            t = max(t, len(w))

        for i in range(len(s), -1, -1):
            for j in range(i, min(len(s), i + t)):
                if trie.search(s, i, j):
                    dp[i] = dp[j + 1]
                    if dp[i]:
                        break

        return dp[0]

Time & Space Complexity

  • Time complexity: O((nt2)+m)O((n * t ^ 2) + m)
  • Space complexity: O(n+(mt))O(n + (m * t))

Where nn is the length of the string ss, mm is the number of words in wordDictwordDict and tt is the maximum length of any word in wordDictwordDict.


Common Pitfalls

Off-by-One Error in DP Array Initialization

The DP array needs size n + 1 to represent the state after processing all characters. Using size n causes index-out-of-bounds when checking dp[n] as the base case.

# Wrong: array too small
dp = [False] * len(s)
dp[len(s)] = True  # IndexError!

# Correct: need n+1 elements
dp = [False] * (len(s) + 1)
dp[len(s)] = True

Checking Substring Beyond String Length

When iterating through possible word matches, failing to check if the word extends beyond the string causes substring errors or incorrect matches.

# Wrong: may go out of bounds
for w in wordDict:
    if s[i:i + len(w)] == w:  # no length check

# Correct: verify word fits in remaining string
for w in wordDict:
    if i + len(w) <= len(s) and s[i:i + len(w)] == w:

Not Converting wordDict to a Set for Efficient Lookup

Using a list for the word dictionary when checking substrings against it results in O(m) lookup time per check, causing TLE on large inputs.

# Wrong: O(m) per lookup
if s[i:j+1] in wordDict:  # wordDict is a list

# Correct: O(1) average lookup
wordSet = set(wordDict)
if s[i:j+1] in wordSet: