Hard草稿★★★★★O(n² + total output) 時間 · O(n · #results) 空間
StringBacktrackingDynamic ProgrammingMemoizationTrie
Patterns🌿 回溯🌴 字典樹
尚未複習過

解法已隱藏 — 先讀題目敘述、自己想想看,再點上方按鈕揭曉。

140Word Break IIBacktrackingHardBacktracking

給定字串 s 與字典 wordDict,在 s 中加空格組成句子,使每個詞都在字典裡。回傳所有可能的句子(任意順序)。

Example:

Input: s = “catsanddog”, wordDict = [“cat”,“cats”,“and”,“sand”,“dog”] Output: [“cats and dog”, “cat sand dog”]

Input: s = “pineapplepenapple”, wordDict = [“apple”,“pen”,“applepen”,“pine”,“pineapple”] Output: [“pine apple pen apple”,“pineapple pen apple”,“pine applepen apple”]

Intuition

TIP

核心思路:回溯列舉所有切法,但用「起點索引 → 該後綴的所有句子」做記憶化,避免同一後綴被重複展開(指數退化)。

Approaches

⭐ Backtracking + Memoization (cache suffix by start) — O(n² + total output) / O(n · #results)
  • Idea: DFS 每個起點嘗試所有字典詞開頭,memo[start] 快取後綴的所有組合
  • Time: 取決於解的數量;記憶化避免重算同一後綴
  • Space: O(n · 解數) - memo 與遞迴
class Solution {
    fun wordBreak(s: String, wordDict: List<String>): List<String> {
        val words = wordDict.toHashSet()
        return backtrack(s, 0, words, HashMap())
    }

    private fun backtrack(
        s: String, start: Int, words: Set<String>, memo: HashMap<Int, List<String>>,
    ): List<String> {
        memo[start]?.let { return it }
        val res = ArrayList<String>()
        if (start == s.length) {
            res.add("")            // 到底:回傳一個空句子作為拼接基底
            return res
        }
        for (end in start + 1..s.length) {
            val word = s.substring(start, end)
            if (word in words) {
                for (sub in backtrack(s, end, words, memo)) {
                    res.add(if (sub.isEmpty()) word else "$word $sub")
                }
            }
        }
        memo[start] = res
        return res
    }
}
Note: Prune with 139's DP first

s = "aaaa...ab"、字典只有 a 這種無解但組合爆炸的輸入,回溯會做大量白工。先用 139 Word Break 的布林 DP 判斷「s 是否可被切分」,不可切就直接回傳空清單,可有效避免最壞情況的指數展開。

🔑 Takeaways