Description

給定一組按照外星語言字母順序排列的單字列表 words。推導出該語言的字母順序。若順序不合法回傳空字串,若有多種合法順序回傳任意一種。

Example:

Input: words = [“wrt”,“wrf”,“er”,“ett”,“rftt”] Output: “wertf”

Intuition#

從相鄰單字的第一個不同字元推導字母先後關係(有向邊),再做拓撲排序。

  • 比較相鄰的兩個單字,找出第一個不同的字元,就確定了一條順序關係(邊)
  • 注意特殊情況:若較長的單字是較短單字的前綴(如 "abc""ab" 前面),則順序不合法
  • 所有邊建好後做拓撲排序,若有環則不合法

Approaches#

1: DFS 拓撲排序#

  • 概念: 建立有向圖,DFS 後序反轉得到拓撲排序,用三色標記偵測環
  • 時間複雜度: O(C),C = 所有單字的總字元數
  • 空間複雜度: O(U + min(U^2, N)),U = 唯一字元數,N = 單字數
class Solution {
    fun alienOrder(words: Array<String>): String {
        val graph = HashMap<Char, MutableSet<Char>>()
        for (word in words) {
            for (c in word) graph.putIfAbsent(c, mutableSetOf())
        }

        for (i in 0 until words.size - 1) {
            val w1 = words[i]
            val w2 = words[i + 1]
            if (w1.length > w2.length && w1.startsWith(w2)) return ""
            for (j in 0 until minOf(w1.length, w2.length)) {
                if (w1[j] != w2[j]) {
                    graph[w1[j]]!!.add(w2[j])
                    break
                }
            }
        }

        val state = HashMap<Char, Int>() // 0: unvisited, 1: visiting, 2: visited
        val result = StringBuilder()

        fun dfs(c: Char): Boolean {
            if (state.getOrDefault(c, 0) == 1) return false // cycle
            if (state.getOrDefault(c, 0) == 2) return true
            state[c] = 1
            for (next in graph[c]!!) {
                if (!dfs(next)) return false
            }
            state[c] = 2
            result.append(c)
            return true
        }

        for (c in graph.keys) {
            if (!dfs(c)) return ""
        }
        return result.reverse().toString()
    }
}

⭐2: BFS 拓撲排序(Kahn’s Algorithm)#

  • 概念: 計算入度,從入度為 0 的字元開始 BFS,出隊順序即為字母順序
  • 時間複雜度: O(C)
  • 空間複雜度: O(U + min(U^2, N))
class Solution {
    fun alienOrder(words: Array<String>): String {
        val graph = HashMap<Char, MutableSet<Char>>()
        val inDegree = HashMap<Char, Int>()

        for (word in words) {
            for (c in word) {
                graph.putIfAbsent(c, mutableSetOf())
                inDegree.putIfAbsent(c, 0)
            }
        }

        for (i in 0 until words.size - 1) {
            val w1 = words[i]
            val w2 = words[i + 1]
            if (w1.length > w2.length && w1.startsWith(w2)) return ""
            for (j in 0 until minOf(w1.length, w2.length)) {
                if (w1[j] != w2[j]) {
                    if (w2[j] !in graph[w1[j]]!!) {
                        graph[w1[j]]!!.add(w2[j])
                        inDegree[w2[j]] = inDegree.getOrDefault(w2[j], 0) + 1
                    }
                    break
                }
            }
        }

        val queue: ArrayDeque<Char> = ArrayDeque()
        for ((c, deg) in inDegree) {
            if (deg == 0) queue.add(c)
        }

        val result = StringBuilder()
        while (queue.isNotEmpty()) {
            val c = queue.removeFirst()
            result.append(c)
            for (next in graph[c]!!) {
                inDegree[next] = inDegree[next]!! - 1
                if (inDegree[next] == 0) queue.add(next)
            }
        }

        return if (result.length == inDegree.size) result.toString() else ""
    }
}

🔑 Takeaways#

  • Pattern: 從排序推導偏序關係 + 拓撲排序
  • 關鍵技巧: 只有相鄰單字的第一個不同字元能推導順序;別忘了處理前綴異常情況("abc""ab" 前面是不合法的)