Hard草稿★★★★★O(N·L·26) 時間 · O(N·L) 空間
BFSBacktrackingStringGraph
Patterns🌿 回溯🌊 廣度優先 BFS🕸️ 圖遍歷・連通分量
尚未複習過

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

126Word Ladder IIGraphsHardGraphs

給定 beginWordendWord 與字典 wordList,每次只能改一個字母且改後的字必須在字典中。回傳所有最短轉換序列(127 只問最短長度,本題要列出所有最短路徑本身)。

Example:

Input: beginWord = “hit”, endWord = “cog”, wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”] Output: [[“hit”,“hot”,“dot”,“dog”,“cog”],[“hit”,“hot”,“lot”,“log”,“cog”]]

Intuition

TIP

核心思路:BFS 逐層擴張找最短距離,過程中記錄每個字的「前驅」(哪些上一層的字能變成它);找到終點後,用回溯沿前驅重建所有最短路徑。

Approaches

1. BFS Shortest Distance + DFS Rebuild — O(N·L·26) / O(N·L)
  • Idea: 先 BFS 算出每個字到 beginWord 的最短距離 dist,再從 begin DFS、每步只走 dist 剛好 +1 的鄰居到 end
  • Time: O(N·L·26) 建距離 + 路徑數 × 長度
  • Space: O(N·L)
class Solution {
    fun findLadders(beginWord: String, endWord: String, wordList: List<String>): List<List<String>> {
        val dict = wordList.toHashSet()
        val res = ArrayList<List<String>>()
        if (endWord !in dict) return res
        dict.add(beginWord)

        val dist = HashMap<String, Int>()
        dist[beginWord] = 0
        val queue = ArrayDeque<String>()
        queue.addLast(beginWord)
        while (queue.isNotEmpty()) {
            val word = queue.removeFirst()
            val d = dist[word]!!
            val chars = word.toCharArray()
            for (k in chars.indices) {
                val orig = chars[k]
                for (c in 'a'..'z') {
                    if (c == orig) continue
                    chars[k] = c
                    val nxt = String(chars)
                    if (nxt in dict && nxt !in dist) { dist[nxt] = d + 1; queue.addLast(nxt) }
                }
                chars[k] = orig
            }
        }
        if (endWord !in dist) return res

        val path = ArrayDeque<String>()
        path.addLast(beginWord)
        fun dfs(word: String) {
            if (word == endWord) { res.add(path.toList()); return }
            val d = dist[word]!!
            val chars = word.toCharArray()
            for (k in chars.indices) {
                val orig = chars[k]
                for (c in 'a'..'z') {
                    if (c == orig) continue
                    chars[k] = c
                    val nxt = String(chars)
                    if (dist[nxt] == d + 1) { path.addLast(nxt); dfs(nxt); path.removeLast() }
                }
                chars[k] = orig
            }
        }
        dfs(beginWord)
        return res
    }
}
⭐ 2. BFS Layered Parents + Backtrack Rebuild — O(N·L·26) / O(N·L)
  • Idea: BFS 記錄 parents[word],再從終點回溯所有路徑
  • Time: O(N·L·26) 建圖(N 字數、L 字長)+ 路徑數 × 長度 回溯
  • Space: O(N·L) - 前驅表與佇列
class Solution {
    fun findLadders(beginWord: String, endWord: String, wordList: List<String>): List<List<String>> {
        val dict = wordList.toHashSet()
        val res = ArrayList<List<String>>()
        if (endWord !in dict) return res

        val parents = HashMap<String, MutableList<String>>()  // word ← 上一層能變成它的字
        var level = hashSetOf(beginWord)
        var found = false

        while (level.isNotEmpty() && !found) {
            for (w in level) dict.remove(w)        // 整層移除,避免回頭、保留同層多前驅
            val next = HashSet<String>()
            for (word in level) {
                val chars = word.toCharArray()
                for (k in chars.indices) {
                    val original = chars[k]
                    for (c in 'a'..'z') {
                        if (c == original) continue
                        chars[k] = c
                        val cand = String(chars)
                        if (cand in dict) {
                            if (cand == endWord) found = true
                            next.add(cand)
                            parents.getOrPut(cand) { ArrayList() }.add(word)
                        }
                    }
                    chars[k] = original
                }
            }
            level = next
        }

        if (found) {
            val path = ArrayDeque<String>()
            path.addLast(endWord)
            backtrack(endWord, beginWord, parents, path, res)
        }
        return res
    }

    private fun backtrack(
        word: String, begin: String, parents: Map<String, List<String>>,
        path: ArrayDeque<String>, res: MutableList<List<String>>,
    ) {
        if (word == begin) {
            res.add(path.reversed())   // 從 endWord 回溯到 beginWord,反轉即正向路徑
            return
        }
        for (parent in parents[word] ?: emptyList()) {
            path.addLast(parent)
            backtrack(parent, begin, parents, path, res)
            path.removeLast()
        }
    }
}

🔑 Takeaways