考題:斷詞
一串字能不能切成字典裡的詞?單純回溯會一再重算同樣的後綴而變成指數時間;記憶化、由下而上的動態規劃、BFS(Breadth-First Search,廣度優先搜尋)都把它降到多項式時間。
解法
輸入
題目
給一個字串 s 和一個單字字典,s 能不能切成一個或多個字典裡的單字?同一個字可以重複用。例如 s = 「leetcode」、字典 = [leet, code] → true。1/32
字典:[cats, dog, sand, and, cat]
s
- c0
- a1
- t2
- s3
- a4
- n5
- d6
- o7
- g8
memo[i]
- ·0
- ·1
- ·2
- ·3
- ·4
- ·5
- ·6
- ·7
- ·8
- ·9
遞迴深度:0
不是單字/目前的索引是單字確定可以切
「catsandog」(從索引 0 開始)能不能切?試每一個可能的第一個字。
字串長度 n
9
查字典次數
22
函式呼叫
5
一種切法
無
| 解法 | 答案 | 查字典次數 | 額外記憶體 | Big O |
|---|---|---|---|---|
| 回溯(暴力遞迴) | false | 24 | 2 格 | O(2ⁿ) |
| 遞迴+記憶化 | false | 22 | 12 格 | O(n²) |
| 由下而上的 DP | false | 20 | 10 格 | O(n²) |
| 在索引上做 BFS | false | 22 | 12 格 | O(n²) |
亮起來的是這一步執行的程式碼
// 1. Backtracking: try every first word, recurse on the rest. Exponential.function wordBreakBacktrack(s: string, words: string[]): boolean { const dict = new Set(words); const solve = (start: number): boolean => { if (start === s.length) return true; for (let end = start + 1; end <= s.length; end++) { if (dict.has(s.slice(start, end)) && solve(end)) return true; } return false; }; return solve(0);} // 2. The same recursion, answering each start index only once. O(n²).function wordBreakMemo(s: string, words: string[]): boolean { const dict = new Set(words); const memo = new Map<number, boolean>(); const solve = (start: number): boolean => { if (memo.has(start)) return memo.get(start)!; if (start === s.length) return true; for (let end = start + 1; end <= s.length; end++) { if (dict.has(s.slice(start, end)) && solve(end)) { memo.set(start, true); return true; } } memo.set(start, false); return false; }; return solve(0);} // 3. Bottom-up: dp[i] says whether the first i characters split. O(n²).function wordBreakDp(s: string, words: string[]): boolean { const dict = new Set(words); const dp = new Array(s.length + 1).fill(false); dp[0] = true; for (let i = 1; i <= s.length; i++) { for (let j = 0; j < i; j++) { if (dp[j] && dict.has(s.slice(j, i))) { dp[i] = true; break; } } } return dp[s.length];} // 4. BFS over indices: a word from i to j is an edge. O(n²).function wordBreakBfs(s: string, words: string[]): boolean { const dict = new Set(words); const seen = new Array(s.length + 1).fill(false); const queue = [0]; seen[0] = true; while (queue.length) { const start = queue.shift()!; for (let end = start + 1; end <= s.length; end++) { if (!dict.has(s.slice(start, end)) || seen[end]) continue; if (end === s.length) return true; seen[end] = true; queue.push(end); } } return false;}模型假設與範圍
- Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。
什麼時候用
- 面試的標準路線:先寫回溯(正確但指數時間),指出同一個起點被重算,加上記憶化,再改寫成由下而上的 DP(Dynamic Programming,動態規劃)。能順便講出 BFS(Breadth-First Search,廣度優先搜尋)的圖論觀點更加分。
- 字典很大、單字都很短時,只試長度不超過最長單字的子字串;或把字典放進 trie,從每個起點沿著 trie 往下走,走不下去就停。
和其他主題的關係
- 由這些組成
- 動態規劃回溯資料結構 · 字首樹
時間與空間複雜度(Big O)
| 操作 | 平均 | 最差 |
|---|---|---|
| 回溯(暴力遞迴) 同一段尾巴被重算無數次;空間 O(n) 遞迴深度 | O(2ⁿ) | O(2ⁿ) |
| 遞迴+記憶化 每個起點只算一次、每次試 n 個終點;O(n) 空間 | O(n²) | O(n²) |
| 由下而上的 DP 兩層迴圈;O(n) 空間,沒有遞迴 | O(n²) | O(n²) |
| 在索引上做 BFS n+1 個點、最多 n² 條邊;O(n) 空間 | O(n²) | O(n²) |
空間:O(n),這裡的次數是查字典的次數;每次要切出子字串並雜湊,長度最多 n,所以實際是 O(n³),把子字串長度限制在最長單字 L 內可降到 O(n·L)
Big O 實測:n 變大時步數怎麼長
數的是:查字典次數,輸入是 n 個 a 加一個 b(答案 false)
| Big O | n = 25 | n = 50 | n = 100 | n = 200 | 成長倍數:實測(理論) | |
|---|---|---|---|---|---|---|
| 遞迴+記憶化 | O(n²) | 351 | 1,326 | 5,151 | 20,301 | ×58 (×64) |
| 由下而上的 DP | O(n²) | 282 | 1,182 | 4,857 | 19,707 | ×70 (×64) |
| 在索引上做 BFS | O(n²) | 351 | 1,326 | 5,151 | 20,301 | ×58 (×64) |
回溯沒有列出來:它是指數級的,上面的成本表已經量過了。
和其他做法比
| n = 4 | n = 8 | n = 12 | n = 16 | |
|---|---|---|---|---|
| 回溯:函式呼叫 | 16 | 224 | 3,096 | 42,744 |
| 回溯:查字典 | 31 | 462 | 6,429 | 88,820 |
| 記憶化:查字典 | 15 | 45 | 91 | 153 |
輸入是 n 個 a 再接一個 b,字典是 [a, aa, aaa, aaaa]:答案是 false,但回溯會試遍所有把 a 切成 1–4 個一組的方法。n 每多 4,呼叫次數大約變成 14 倍;n = 16 時回溯查了 88,820 次,記憶化只查了 153 次。
真實世界裡的它
- LeetCode 139(Word Break);變形 LeetCode 140 要列出所有切法,答案本身就可能是指數多個,只能回溯加記憶化。
- 真實用途:中文、日文、泰文沒有空格,斷詞(把一串字切成詞)是搜尋引擎和輸入法的第一步,做法是同一個 DP 再加上每個詞的機率。
取捨與陷阱
- DP 的定義要講清楚:dp[i] 是「前 i 個字元可以切」,所以陣列長度是 n + 1、dp[0] = true。差一的錯誤最常出在這裡。
- 記憶化要記失敗,不只是成功:回溯慢就是因為一直重算失敗的尾巴。
- BFS 沒有「看過」標記時,同一個索引會被放進佇列很多次,又變回指數級。