HARD Lab · Dynamic Programming · 32

139. Word Break

Build a boolean DP table where dp[i] asks “can s[0..i) be segmented into dictionary words?” — seeding dp[0] = true and propagating forward with a nested split loop.

MediumDPHash SetTypeScript

PROBLEM What we're solving

Given a string s and a list of words wordDict, return true if s can be split into a sequence of one or more dictionary words. Words may be reused.

Example: s = "leetcode", wordDict = ["leet", "code"] true because "leet" + "code" covers the whole string. But s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] false: no partition reaches the end.

KEY IDEA A reachable prefix can extend itself

Insight → define dp[i] = “the first i characters of sare segmentable.” Then dp[i] is true iff there exists some split point j < i where both dp[j] is true (the prefix up to j is already solved) and s[j..i) is a dictionary word. The empty prefix dp[0] = true is the seed that starts the chain.

RECURRENCE The DP formula

  • 0 · Seed. Set dp[0] = true. The empty string needs no words — this anchors everything.
  • 1 · Outer loop i = 1..n. We're asking: can s[0..i) be segmented?
  • 2 · Inner loop j = 0..i-1. Try every split point. If dp[j] is already true, the prefix to j is safe — we only need s[j..i) to be a word.
  • 3 · Propagate. If dp[j] && dict.has(s.slice(j, i)), set dp[i] = true and break — one valid split is enough.
  • 4 · Answer. Return dp[n].
Classic confusion → people initialize dp[0] = false and wonder why nothing propagates. The empty prefix dp[0] = trueisn't an edge case — it's the indispensable seed. Without it, even a single-word string like "leet" would return false.

COST Complexity & alternatives

Brute-force recursion
O(2ⁿ)
Exponential: re-solves the same substrings.
DP + hash set
O(n²)
O(n²) time (n² substrings), O(n + w) space for the table + dictionary set.

Alternatives

Memoized DFS: same O(n²) complexity; top-down instead of bottom-up, which some find more natural to write. Trie: replace the hash set with a trie over the dict to speed up repeated prefix lookups — helpful when |wordDict| is huge but word lengths are short. The DP approach is the cleanest for interviews.

Pattern transfer →the same “reachable prefix extends itself” pattern appears in Word Break II (reconstruct all sentences), Decode Ways (each digit expands valid decodings), Palindrome Partitioning II (dp[i] = min cuts to reach i), and Concatenated Words (each word is a word-break sub-problem).

RUN IT Fill dp[0..n]: can s[0..i) be segmented?

step 0 / 39
STARTSeed: dp[0] = true — the empty prefix is always segmentable. We'll fill dp[1..8] left to right.
s =l0e1e2t3c4o5d6e7
State
true
dp[0]
i
j
dict match / dp truecurrent span [j, i)dp[j] = true (reachable prefix)not in dict / dp false
slowfast

TYPESCRIPT The solution, annotated

wordBreak.ts
function wordBreak(s: string, wordDict: string[]): boolean {
  const dict = new Set(wordDict);
  const n = s.length;
  const dp: boolean[] = new Array(n + 1).fill(false);
  dp[0] = true; // empty prefix is always segmentable

  for (let i = 1; i <= n; i++) {
    for (let j = 0; j < i; j++) {
      if (dp[j] && dict.has(s.slice(j, i))) {
        dp[i] = true;
        break; // one valid split is enough
      }
    }
  }

  return dp[n];
}

Reading it block by block

Line 2 — build the dictionary set. A Set gives O(1) average lookup for dict.has(word). Using the raw array would be O(w) per lookup and degrade the overall complexity.
Lines 4–5 — allocate and seed. dp is length n + 1 so index i directly represents the first i characters. Setting dp[0] = true expresses: the empty string needs zero words — always segmentable. This seed is what lets the first real word propagate.
Lines 7–14 — fill the table. For each end-position i, scan every split point j. If the prefix to j is already reachable (dp[j]) and the slice s[j..i) is a dictionary word, mark dp[i] = true. The breakis a small optimization — once we find one valid split, there's no need to check others.
Line 16 — return the answer. dp[n]asks exactly “can the whole string be segmented?” — the original question, answered by building up from smaller sub-answers.
Complexity → O(n²) time: the nested loops produce n²/2 substring checks, each O(1) in the set. O(n + w) space: the dp array of length n + 1, plus the set holding all characters of all w dictionary words.

INTERVIEWFollow-ups they'll ask

  • "Return all valid sentences (Word Break II)?" Use memoized DFS or backtracking from each dp[i] = true position to reconstruct the actual word sequences — watch for exponential output size.
  • "Words can't be reused?" Track which words have been used in a visited set; the recurrence becomes a subset-sum variant and complexity grows significantly.
  • "What if the dictionary is enormous?" A trie over the dictionary lets you scan s forward from each reachable index without hashing every substring — reduces repeated prefix work.
  • "Can you do it top-down instead?" Yes — memoized recursion on canBreak(i) (can s[i..n) be segmented?) is equivalent and some find it easier to derive.
  • "What's the brute-force?" Enumerate every possible partition with recursion and no memoization — O(2ⁿ) because at each index you branch on whether to cut or continue. DP caches the sub-answers to avoid the blowup.

MNEMONIC The one-liner

"dp[0] plants the flag; every reachable prefix passes the torch if a dict word bridges the gap."

TRIGGERS When you see ___ → reach for ___

"can string be segmented into words"boolean DP + hash set
partition / split string into valid piecesdp[i] = OR over j < i of dp[j] && word(j,i)
reuse allowed, reconstruct sentencesWord Break II: memoized DFS
similar: decode ways, palindrome min-cutssame reachable-prefix DP shape

SKELETON The reusable shape

skeleton.ts
function wordBreak(s: string, wordDict: string[]): boolean {
  const dict = new Set(wordDict);
  const n = s.length;
  const dp = new Array(n + 1).fill(false);
  dp[0] = true; // seed: empty prefix
  for (let i = 1; i <= n; i++) {
    for (let j = 0; j < i; j++) {
      if (dp[j] && dict.has(s.slice(j, i))) {
        dp[i] = true;
        break;
      }
    }
  }
  return dp[n];
}

FLASHCARDS Tap to flip

What does dp[i] mean in Word Break?
dp[i] = true iff s[0..i) (the first i chars) can be fully segmented into dictionary words.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
What is the time complexity of the DP solution for Word Break (string length n, dictionary lookup O(1))?
QUESTION 02
Why is dp[0] = true essential?
QUESTION 03
Trace: s = "leetcode", wordDict = ["leet", "code"]. What is dp[4]?
QUESTION 04
What does breaking out of the inner j-loop early achieve?
QUESTION 05
s = "applepenapple", wordDict = ["apple", "pen"]. What does the algorithm return?
QUESTION 06
Which data structure makes the inner dict lookup O(1)?
QUESTION 07
How many entries does the dp array have, and what do the indices represent?