Description

給定一個字串陣列 words(不含重複),找出所有可以由陣列中至少兩個較短字串串接而成的字串。

Example:

Input: words = [“cat”,“cats”,“catsdogcats”,“dog”,“dogcatsdog”,“hippopotamuses”,“rat”,“ratcatdogcat”] Output: [“catsdogcats”,“dogcatsdog”,“ratcatdogcat”]

Intuition#

對每個字串做 Word Break 判斷:能否被字典中的其他(更短)字串完全拼接。

  • 本質上是對每個字串執行 LeetCode 139 Word Break 的檢查
  • 先按長度排序,逐步加入字典
  • 對每個字串用 DP 判斷是否能被已有字典中的字串拼接

Approaches#

1: DP Word Break for Each Word#

  • 概念: 按長度排序,對每個字串做 Word Break DP 檢查
  • 時間複雜度: O(n * L^2) 其中 L 為最長字串長度
  • 空間複雜度: O(n * L)
class Solution {
    fun findAllConcatenatedWordsInADict(words: Array<String>): List<String> {
        val dict = HashSet<String>()
        val result = mutableListOf<String>()
        words.sortBy { it.length }

        for (word in words) {
            if (word.isEmpty()) continue
            if (canForm(word, dict)) {
                result.add(word)
            }
            dict.add(word)
        }
        return result
    }

    private fun canForm(word: String, dict: Set<String>): Boolean {
        if (dict.isEmpty()) return false
        val n = word.length
        val dp = BooleanArray(n + 1)
        dp[0] = true
        for (i in 1..n) {
            for (j in 0 until i) {
                if (dp[j] && word.substring(j, i) in dict) {
                    dp[i] = true
                    break
                }
            }
        }
        return dp[n]
    }
}

⭐2: Trie + DFS#

  • 概念: 用 Trie 加速前綴匹配,DFS 搜尋所有可能的分割方式
  • 時間複雜度: O(n * L^2)
  • 空間複雜度: O(n * L) (Trie 空間)
class Solution {
    class TrieNode {
        val children = arrayOfNulls<TrieNode>(26)
        var isEnd = false
    }

    fun findAllConcatenatedWordsInADict(words: Array<String>): List<String> {
        val root = TrieNode()
        for (w in words) {
            if (w.isEmpty()) continue
            var node = root
            for (c in w) {
                val idx = c - 'a'
                if (node.children[idx] == null) node.children[idx] = TrieNode()
                node = node.children[idx]!!
            }
            node.isEnd = true
        }

        val result = mutableListOf<String>()
        for (word in words) {
            if (word.isNotEmpty() && dfs(word, 0, root, 0)) {
                result.add(word)
            }
        }
        return result
    }

    private fun dfs(word: String, start: Int, root: TrieNode, count: Int): Boolean {
        if (start == word.length) return count >= 2
        var node = root
        for (i in start until word.length) {
            val idx = word[i] - 'a'
            node = node.children[idx] ?: return false
            if (node.isEnd && dfs(word, i + 1, root, count + 1)) return true
        }
        return false
    }
}

🔑 Takeaways#

  • Pattern: Word Break 推廣 — 對集合中每個字串做分詞檢查
  • 關鍵技巧: 按長度排序後逐步加入字典,確保只用更短的字串來組合;Trie 可加速前綴查找但本題 HashSet 已足夠高效