Word Break

Difficulty: Medium

Given a string s and a list of words wordDict, determine whether s can be split into a sequence of one or more words that all appear in wordDict. The same word from the dictionary can be reused as many times as needed.

Examples

Input: s = "leetcode", wordDict = ["leet", "code"]
Output: true

"leetcode" splits into "leet" + "code".

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

"applepenapple" splits into "apple" + "pen" + "apple", reusing "apple".

Input: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
Output: false

No combination of the given words covers the whole string cleanly — "catsandog" always leaves a leftover chunk like "og" that isn't a word.

Constraints

  • 1 <= s.length <= 300

  • 1 <= wordDict.length <= 1000

  • s and every word in wordDict consist of lowercase English letters

Approach

Ask the question one position at a time: starting from index i, can the rest of the string be broken into dictionary words? That's true exactly when some dictionary word matches the string starting right at i, and the remainder after that word can also be broken — which is the exact same question, just starting further along.

That gives a clean recursive definition, with the empty remainder (past the end of the string) as the trivially true base case. Plain recursion re-asks the same "can this suffix be broken?" question many times through different paths, so caching each answer — or building them up from the end of the string backward — avoids the repeated work.

Solutions

Brute Force — Plain Recursion

From each starting position, try every dictionary word that matches there, and recurse on whatever's left. Correct, but the same starting positions get re-explored repeatedly through different word choices.

function wordBreak(s, wordDict) {
  const words = new Set(wordDict);

  function canBreak(start) {
    if (start === s.length) return true;

    for (const word of words) {
      if (s.startsWith(word, start) && canBreak(start + word.length)) {
        return true;
      }
    }
    return false;
  }

  return canBreak(0);
}

Time: O(2^n * m) — exponential branching, m = average word length for the startsWith checks · Space: O(n) — recursion depth

Optimal — Bottom-Up Tabulation

Work backward from the end of the string. dp[i] records whether the suffix starting at i can be broken into dictionary words, built from the already-solved dp values for positions after it.

function wordBreak(s, wordDict) {
  const words = new Set(wordDict);
  const n = s.length;

  // dp[i] = can the suffix s[i..n) be broken into dictionary words?
  const dp = new Array(n + 1).fill(false);
  dp[n] = true; // the empty suffix is trivially breakable

  for (let i = n - 1; i >= 0; i--) {
    for (const word of words) {
      if (s.startsWith(word, i) && dp[i + word.length]) {
        dp[i] = true;
        break;
      }
    }
  }

  return dp[0];
}

Time: O(n * w * m) — n positions, w dictionary words, m average word length for matching · Space: O(n + w) — the dp array plus the word set