Alien Dictionary
Problem
Given a list of words sorted lexicographically according to an unknown alien language, return a string representing the characters in the alien language sorted in lexicographical order. Return an empty string if no valid ordering exists.
- 1 ≤ words.length ≤ 100
- 1 ≤ words[i].length ≤ 100
- words[i] consists of only lowercase English letters
Example
words = ["wrt", "wrf", "er", "ett", "rftt"]"wertf"The algorithm compares adjacent words to infer ordering constraints between characters. For example, comparing "wrt" and "wrf" reveals that 't' comes before 'f'. Comparing "wrf" and "er" shows 'w' comes before 'e'. These pairwise constraints build a directed graph representing character precedence. The critical moment is detecting an invalid prefix case: if a longer word precedes a shorter word that is a prefix of it (e.g., "abc" before "ab"), the ordering is invalid, and the algorithm returns an empty string immediately.
Approach
Straightforward Solution
A naive approach might attempt to guess character orderings or try all permutations, which is infeasible due to combinatorial explosion.
Core Observation
The lexicographical order of words implies a partial order of characters. Comparing adjacent words reveals direct precedence relations between characters, which can be represented as edges in a directed graph.
Path to Optimal
The key insight is to model the problem as a graph of character precedence and perform a topological sort to find a valid linear ordering. Detecting invalid prefix cases early prevents futile computation. Constructing the graph by comparing adjacent words and extracting the first differing character pair yields the minimal necessary edges. The topological sort then detects cycles, which indicate contradictory orderings.
Optimal Approach
Build a directed graph where nodes are characters and edges represent precedence constraints derived from adjacent word comparisons. Detect invalid prefix cases by checking if a longer word precedes its prefix. Use DFS-based topological sort with cycle detection to produce a valid character order or return empty string if a cycle exists.
Time
O(C + V)
Building the graph requires comparing adjacent words and their characters, which takes O(C) where C is the total number of characters across all words. The DFS topological sort visits each node and edge once, resulting in O(V + E) time, where V is the number of unique characters and E is the number of edges. Since E ≤ C, total complexity is O(C + V).
Space
O(V + E)
The adjacency list stores edges for each character, requiring O(V + E) space. The recursion stack and visited dictionary use O(V) space. This auxiliary space is necessary to represent the graph and track traversal state.
Pattern Spotlight
Topological Sort (Cycle Detection in Directed Graph)
When inferring order from pairwise precedence constraints, represent characters as graph nodes and edges as 'comes before' relations; then use DFS with cycle detection to find a valid linear order or detect contradictions.
Solution
| 1 | class Solution:
|
| 2 | def alienOrder(self, words: list[str]) -> str:
|
| 3 | adj = { c: set() for w in words for c in w }
|
| 4 |
|
| 5 | for i in range(len(words) - 1):
|
| 6 | w1, w2 = words[i], words[i + 1]
|
| 7 | min_len = min(len(w1), len(w2))
|
| 8 | if len(w1) > len(w2) and w1[:min_len] == w2[:min_len]:
|
| 9 | return ""
|
| 10 | for j in range(min_len):
|
| 11 | if w1[j] != w2[j]:
|
| 12 | adj[w1[j]].add(w2[j])
|
| 13 | break
|
| 14 |
|
| 15 | visit = {}
|
| 16 | res = []
|
| 17 |
|
| 18 | def dfs(c):
|
| 19 | if c in visit:
|
| 20 | return visit[c]
|
| 21 |
|
| 22 | visit[c] = True
|
| 23 | for nei in adj[c]:
|
| 24 | if dfs(nei):
|
| 25 | return True
|
| 26 |
|
| 27 | visit[c] = False
|
| 28 | res.append(c)
|
| 29 |
|
| 30 | for c in adj:
|
| 31 | if dfs(c):
|
| 32 | return ""
|
| 33 |
|
| 34 | return "".join(res[::-1]) |
Step-by-Step Solution
Construct Directed Graph of Character Precedence from Adjacent Words
| 3 | adj = { c: set() for w in words for c in w }
|
| 5 | for i in range(len(words) - 1):
|
| 6 | w1, w2 = words[i], words[i + 1]
|
| 7 | min_len = min(len(w1), len(w2))
|
| 8 | if len(w1) > len(w2) and w1[:min_len] == w2[:min_len]:
|
| 9 | return ""
|
| 10 | for j in range(min_len):
|
| 11 | if w1[j] != w2[j]:
|
| 12 | adj[w1[j]].add(w2[j])
|
| 13 | break
|
Objective
To build a graph where each character points to characters that must come after it, based on the first differing character between adjacent words.
Key Insight
Comparing adjacent words reveals the minimal necessary ordering constraints. The first position where two words differ determines a direct precedence edge from the character in the first word to the character in the second. This approach avoids over-constraining the graph and ensures only valid edges are added. Detecting the invalid prefix case here prevents futile graph construction.
Interview Quick-Check
Core Logic
Build adjacency sets for each character by comparing adjacent words and adding an edge from the first differing character in the first word to that in the second.
Common Pitfalls & Bugs
Failing to detect the invalid prefix case where a longer word precedes its prefix leads to incorrect results or infinite loops.
State & Boundaries
Stop comparing characters at the first difference to avoid adding incorrect edges.
Perform DFS-Based Topological Sort with Cycle Detection
| 15 | visit = {}
|
| 16 | res = []
|
| 18 | def dfs(c):
|
| 19 | if c in visit:
|
| 20 | return visit[c]
|
| 22 | visit[c] = True
|
| 23 | for nei in adj[c]:
|
| 24 | if dfs(nei):
|
| 25 | return True
|
| 27 | visit[c] = False
|
| 28 | res.append(c)
|
| 30 | for c in adj:
|
| 31 | if dfs(c):
|
| 32 | return ""
|
| 34 | return "".join(res[::-1]) |
Objective
To traverse the graph depth-first, detect cycles, and build the character order in reverse postorder.
Key Insight
DFS traversal marks nodes as visiting or visited to detect cycles: encountering a node marked visiting indicates a cycle and invalid ordering. Appending characters to the result list after exploring all neighbors ensures a valid topological order when reversed. This approach efficiently detects contradictions and produces the correct order or returns empty string if impossible.
Interview Quick-Check
Core Logic
Use DFS with a visit state dictionary to detect cycles and append characters post-recursion to build a valid topological order.
State & Boundaries
Mark nodes as visiting (True) when recursion starts and visited (False) when recursion ends to track traversal state.
Common Pitfalls & Bugs
Not reversing the result list before returning leads to incorrect character order.
Line Analysis
This solution has 12 Critical lines interviewers watch for.
if len(w1) > len(w2) and w1[:min_len] == w2[:min_len]:
Check for invalid prefix case where longer word precedes its prefix.
This condition violates lexicographical order rules, so detecting it early allows immediate termination with an empty string, preventing incorrect results.
if dfs(c):
If DFS detects a cycle starting from this character, return empty string.
Cycle detection invalidates the ordering, so the algorithm must terminate early with no solution.
return ""
Return empty string upon detecting invalid prefix ordering.
Returning immediately signals no valid character order exists due to contradictory input ordering.
if w1[j] != w2[j]:
Check if characters at current position differ.
Identifying the first differing character pair is critical to establishing a precedence edge in the graph.
adj[w1[j]].add(w2[j])
Add an edge from the character in the first word to the character in the second word.
This edge encodes the ordering constraint that the first character must come before the second in the alien language.
if c in visit:
Return visit state if character has been visited or is currently visiting.
This line prevents redundant work and detects cycles by returning True if a cycle is found (node is currently visiting).
return visit[c]
Mark character as currently visiting (True).
Marking as visiting signals the start of exploration; encountering this state again indicates a cycle.
for nei in adj[c]:
If a cycle is detected in any neighbor, propagate True to indicate failure.
Early termination upon cycle detection prevents unnecessary traversal and signals invalid ordering.
if dfs(nei):
Return True immediately if a cycle is found.
This return propagates cycle detection up the recursion stack, aborting the topological sort.
res.append(c)
Append character to result list after visiting all neighbors.
Appending post-recursion ensures characters appear after all their dependencies, building a valid topological order in reverse.
return ""
Return empty string upon cycle detection.
Signals to the caller that no valid alien dictionary ordering exists due to contradictory constraints.
return "".join(res[::-1])
Return the reversed result list as a string representing the alien dictionary order.
Reversing the postorder traversal list produces the correct topological order of characters consistent with all precedence constraints.
adj = { c: set() for w in words for c in w }
Initialize adjacency list mapping each character to an empty set of neighbors.
Creating an adjacency set for every character ensures all characters appear as nodes in the graph, even if they have no outgoing edges, which is essential for a complete topological sort.
for j in range(min_len):
Iterate through characters up to the minimum length to find first differing character.
The first differing character between adjacent words determines a direct precedence edge; subsequent characters do not provide valid ordering information.
visit = {}
Initialize a dictionary to track visit state of characters during DFS.
Tracking visit state is essential for cycle detection and preventing redundant traversals during topological sort.
def dfs(c):
Define DFS function to traverse graph and detect cycles.
DFS enables depth-first exploration of dependencies, allowing cycle detection and ordering construction in one pass.
visit[c] = True
Recursively visit all neighbors of the current character.
Exploring neighbors enforces all precedence constraints and detects cycles in downstream nodes.
visit[c] = False
Mark character as fully visited (False).
Marking as visited indicates the node and its descendants have been processed without cycles, allowing reuse of this state.
for i in range(len(words) - 1):
Iterate through pairs of adjacent words to infer ordering constraints.
Comparing adjacent words is the only way to extract direct precedence relations between characters, as the input words are sorted lexicographically according to the alien language.
break
Break out of the loop after adding the first precedence edge.
Only the first differing character pair matters for ordering; further characters do not affect the relative order of these two words.
res = []
Initialize a list to accumulate the topological order of characters.
Characters are appended post-recursion to build the order in reverse; this list will be reversed before returning the final result.
for c in adj:
Iterate over all characters in the graph to initiate DFS.
Ensures all disconnected components and isolated nodes are processed for a complete ordering.
min_len = min(len(w1), len(w2))
Determine the minimum length of the two words for safe comparison.
Comparisons beyond the shorter word's length are invalid and can cause index errors; this also helps detect invalid prefix cases.
return True
End of neighbor iteration.
Completing neighbor exploration without cycle detection means current node is safe to finalize.
w1, w2 = words[i], words[i + 1]
Assign the current and next words for comparison.
Storing the two words simplifies subsequent character-by-character comparison and prefix checks.
Test Your Understanding
Why must the algorithm detect the invalid prefix case where a longer word precedes its prefix, and how does this affect the ordering?
Because in lexicographical order, a prefix word must come before any longer word starting with that prefix. If a longer word appears before its prefix, no valid character ordering can satisfy this, so the algorithm must return an empty string immediately to indicate no valid ordering exists.
Related Problems
Graphs pattern
Don't just read it. Drill it.
Reconstruct Alien Dictionary from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.
Unlock the Alien Dictionary drill