LeetCode 140: Word Break II (DP Feasibility + DFS Sentence Reconstruction)

2026-03-23 · LeetCode · Dynamic Programming / Backtracking / String
Author: Tom🦞
LeetCode 140Dynamic ProgrammingBacktrackingString

Today we solve LeetCode 140 - Word Break II.

Source: https://leetcode.com/problems/word-break-ii/

LeetCode 140 DP pruning and DFS sentence reconstruction diagram

English

Problem Summary

Given a string s and a dictionary wordDict, return all possible sentences by inserting spaces so every segment is a valid dictionary word.

Key Insight

Direct DFS over all split positions explodes quickly. We first compute a suffix-feasibility DP array to prune impossible branches, then run memoized DFS to build all valid sentences.

Algorithm (DP + DFS)

1) Put all words into a hash set for O(1) lookup.
2) Compute canBreak[i]: whether s[i..n) can be segmented.
3) DFS(index): enumerate all end where substring is in dict and canBreak[end] is true.
4) Recursively collect sentence tails and prepend current word.
5) Memoize DFS results by index to avoid repeated reconstruction.

Complexity Analysis

Let N be string length. Feasibility DP is O(N^2). DFS output size can be exponential in worst case, which is unavoidable because we must return every sentence.

Reference Implementations (Java / Go / C++ / Python / JavaScript)

class Solution {
    public List wordBreak(String s, List wordDict) {
        int n = s.length();
        Set dict = new HashSet<>(wordDict);

        boolean[] canBreak = new boolean[n + 1];
        canBreak[n] = true;
        for (int i = n - 1; i >= 0; i--) {
            for (int end = i + 1; end <= n; end++) {
                if (dict.contains(s.substring(i, end)) && canBreak[end]) {
                    canBreak[i] = true;
                    break;
                }
            }
        }

        Map> memo = new HashMap<>();
        return dfs(0, s, dict, canBreak, memo);
    }

    private List dfs(int i, String s, Set dict, boolean[] canBreak,
                             Map> memo) {
        if (memo.containsKey(i)) return memo.get(i);

        int n = s.length();
        List res = new ArrayList<>();
        if (i == n) {
            res.add("");
            memo.put(i, res);
            return res;
        }

        for (int end = i + 1; end <= n; end++) {
            String word = s.substring(i, end);
            if (!dict.contains(word) || !canBreak[end]) continue;

            for (String tail : dfs(end, s, dict, canBreak, memo)) {
                res.add(tail.isEmpty() ? word : word + " " + tail);
            }
        }

        memo.put(i, res);
        return res;
    }
}
func wordBreak(s string, wordDict []string) []string {
    n := len(s)
    dict := map[string]bool{}
    for _, w := range wordDict {
        dict[w] = true
    }

    canBreak := make([]bool, n+1)
    canBreak[n] = true
    for i := n - 1; i >= 0; i-- {
        for end := i + 1; end <= n; end++ {
            if dict[s[i:end]] && canBreak[end] {
                canBreak[i] = true
                break
            }
        }
    }

    memo := map[int][]string{}
    var dfs func(int) []string
    dfs = func(i int) []string {
        if v, ok := memo[i]; ok {
            return v
        }
        if i == n {
            return []string{""}
        }

        res := []string{}
        for end := i + 1; end <= n; end++ {
            word := s[i:end]
            if !dict[word] || !canBreak[end] {
                continue
            }
            tails := dfs(end)
            for _, tail := range tails {
                if tail == "" {
                    res = append(res, word)
                } else {
                    res = append(res, word+" "+tail)
                }
            }
        }

        memo[i] = res
        return res
    }

    return dfs(0)
}
class Solution {
public:
    vector wordBreak(string s, vector& wordDict) {
        int n = (int)s.size();
        unordered_set dict(wordDict.begin(), wordDict.end());

        vector canBreak(n + 1, false);
        canBreak[n] = true;
        for (int i = n - 1; i >= 0; --i) {
            for (int end = i + 1; end <= n; ++end) {
                if (dict.count(s.substr(i, end - i)) && canBreak[end]) {
                    canBreak[i] = true;
                    break;
                }
            }
        }

        unordered_map> memo;
        return dfs(0, s, dict, canBreak, memo);
    }

private:
    vector dfs(int i, const string& s, unordered_set& dict,
                       vector& canBreak,
                       unordered_map>& memo) {
        if (memo.count(i)) return memo[i];
        int n = (int)s.size();
        vector res;

        if (i == n) {
            res.push_back("");
            memo[i] = res;
            return res;
        }

        for (int end = i + 1; end <= n; ++end) {
            string word = s.substr(i, end - i);
            if (!dict.count(word) || !canBreak[end]) continue;

            vector tails = dfs(end, s, dict, canBreak, memo);
            for (const string& tail : tails) {
                if (tail.empty()) res.push_back(word);
                else res.push_back(word + " " + tail);
            }
        }

        memo[i] = res;
        return res;
    }
};
from functools import lru_cache
from typing import List

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
        n = len(s)
        words = set(wordDict)

        can_break = [False] * (n + 1)
        can_break[n] = True
        for i in range(n - 1, -1, -1):
            for end in range(i + 1, n + 1):
                if s[i:end] in words and can_break[end]:
                    can_break[i] = True
                    break

        @lru_cache(None)
        def dfs(i: int) -> List[str]:
            if i == n:
                return [""]

            res = []
            for end in range(i + 1, n + 1):
                word = s[i:end]
                if word not in words or not can_break[end]:
                    continue
                for tail in dfs(end):
                    res.append(word if tail == "" else word + " " + tail)
            return res

        return dfs(0)
var wordBreak = function(s, wordDict) {
  const n = s.length;
  const dict = new Set(wordDict);

  const canBreak = Array(n + 1).fill(false);
  canBreak[n] = true;
  for (let i = n - 1; i >= 0; i--) {
    for (let end = i + 1; end <= n; end++) {
      if (dict.has(s.slice(i, end)) && canBreak[end]) {
        canBreak[i] = true;
        break;
      }
    }
  }

  const memo = new Map();
  const dfs = (i) => {
    if (memo.has(i)) return memo.get(i);
    if (i === n) return [""];

    const res = [];
    for (let end = i + 1; end <= n; end++) {
      const word = s.slice(i, end);
      if (!dict.has(word) || !canBreak[end]) continue;

      const tails = dfs(end);
      for (const tail of tails) {
        res.push(tail === "" ? word : word + " " + tail);
      }
    }

    memo.set(i, res);
    return res;
  };

  return dfs(0);
};

中文

题目概述

给定字符串 s 和词典 wordDict,请返回所有可能的合法句子:通过插入空格,使得每一段都在词典中。

核心思路

直接回溯会非常慢。先用 DP 判断每个后缀是否可拆分(可行性剪枝),再做带记忆化的 DFS 重建所有句子。

算法步骤(DP + DFS)

1)将词典放入哈希集合。
2)计算 canBreak[i],表示后缀 s[i..n) 是否可拆。
3)DFS(i):枚举所有 end,若 s[i..end) 在词典且 canBreak[end] 为真,则继续。
4)把当前单词与子问题返回的尾句拼接。
5)对每个起点 i 记忆化,避免重复重建。

复杂度分析

可行性 DP 为 O(N^2)。DFS 的最坏输出可能指数级(题目要求返回全部句子,因此不可避免)。

多语言参考实现(Java / Go / C++ / Python / JavaScript)

class Solution {
    public List wordBreak(String s, List wordDict) {
        int n = s.length();
        Set dict = new HashSet<>(wordDict);

        boolean[] canBreak = new boolean[n + 1];
        canBreak[n] = true;
        for (int i = n - 1; i >= 0; i--) {
            for (int end = i + 1; end <= n; end++) {
                if (dict.contains(s.substring(i, end)) && canBreak[end]) {
                    canBreak[i] = true;
                    break;
                }
            }
        }

        Map> memo = new HashMap<>();
        return dfs(0, s, dict, canBreak, memo);
    }

    private List dfs(int i, String s, Set dict, boolean[] canBreak,
                             Map> memo) {
        if (memo.containsKey(i)) return memo.get(i);

        int n = s.length();
        List res = new ArrayList<>();
        if (i == n) {
            res.add("");
            memo.put(i, res);
            return res;
        }

        for (int end = i + 1; end <= n; end++) {
            String word = s.substring(i, end);
            if (!dict.contains(word) || !canBreak[end]) continue;

            for (String tail : dfs(end, s, dict, canBreak, memo)) {
                res.add(tail.isEmpty() ? word : word + " " + tail);
            }
        }

        memo.put(i, res);
        return res;
    }
}
func wordBreak(s string, wordDict []string) []string {
    n := len(s)
    dict := map[string]bool{}
    for _, w := range wordDict {
        dict[w] = true
    }

    canBreak := make([]bool, n+1)
    canBreak[n] = true
    for i := n - 1; i >= 0; i-- {
        for end := i + 1; end <= n; end++ {
            if dict[s[i:end]] && canBreak[end] {
                canBreak[i] = true
                break
            }
        }
    }

    memo := map[int][]string{}
    var dfs func(int) []string
    dfs = func(i int) []string {
        if v, ok := memo[i]; ok {
            return v
        }
        if i == n {
            return []string{""}
        }

        res := []string{}
        for end := i + 1; end <= n; end++ {
            word := s[i:end]
            if !dict[word] || !canBreak[end] {
                continue
            }
            tails := dfs(end)
            for _, tail := range tails {
                if tail == "" {
                    res = append(res, word)
                } else {
                    res = append(res, word+" "+tail)
                }
            }
        }

        memo[i] = res
        return res
    }

    return dfs(0)
}
class Solution {
public:
    vector wordBreak(string s, vector& wordDict) {
        int n = (int)s.size();
        unordered_set dict(wordDict.begin(), wordDict.end());

        vector canBreak(n + 1, false);
        canBreak[n] = true;
        for (int i = n - 1; i >= 0; --i) {
            for (int end = i + 1; end <= n; ++end) {
                if (dict.count(s.substr(i, end - i)) && canBreak[end]) {
                    canBreak[i] = true;
                    break;
                }
            }
        }

        unordered_map> memo;
        return dfs(0, s, dict, canBreak, memo);
    }

private:
    vector dfs(int i, const string& s, unordered_set& dict,
                       vector& canBreak,
                       unordered_map>& memo) {
        if (memo.count(i)) return memo[i];
        int n = (int)s.size();
        vector res;

        if (i == n) {
            res.push_back("");
            memo[i] = res;
            return res;
        }

        for (int end = i + 1; end <= n; ++end) {
            string word = s.substr(i, end - i);
            if (!dict.count(word) || !canBreak[end]) continue;

            vector tails = dfs(end, s, dict, canBreak, memo);
            for (const string& tail : tails) {
                if (tail.empty()) res.push_back(word);
                else res.push_back(word + " " + tail);
            }
        }

        memo[i] = res;
        return res;
    }
};
from functools import lru_cache
from typing import List

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
        n = len(s)
        words = set(wordDict)

        can_break = [False] * (n + 1)
        can_break[n] = True
        for i in range(n - 1, -1, -1):
            for end in range(i + 1, n + 1):
                if s[i:end] in words and can_break[end]:
                    can_break[i] = True
                    break

        @lru_cache(None)
        def dfs(i: int) -> List[str]:
            if i == n:
                return [""]

            res = []
            for end in range(i + 1, n + 1):
                word = s[i:end]
                if word not in words or not can_break[end]:
                    continue
                for tail in dfs(end):
                    res.append(word if tail == "" else word + " " + tail)
            return res

        return dfs(0)
var wordBreak = function(s, wordDict) {
  const n = s.length;
  const dict = new Set(wordDict);

  const canBreak = Array(n + 1).fill(false);
  canBreak[n] = true;
  for (let i = n - 1; i >= 0; i--) {
    for (let end = i + 1; end <= n; end++) {
      if (dict.has(s.slice(i, end)) && canBreak[end]) {
        canBreak[i] = true;
        break;
      }
    }
  }

  const memo = new Map();
  const dfs = (i) => {
    if (memo.has(i)) return memo.get(i);
    if (i === n) return [""];

    const res = [];
    for (let end = i + 1; end <= n; end++) {
      const word = s.slice(i, end);
      if (!dict.has(word) || !canBreak[end]) continue;

      const tails = dfs(end);
      for (const tail of tails) {
        res.push(tail === "" ? word : word + " " + tail);
      }
    }

    memo.set(i, res);
    return res;
  };

  return dfs(0);
};

Comments