DSA sheet · Graphs · Topological sort (BFS) pattern
Alien Dictionary
This is the last topological sort problem in the sheet, and the trickiest one, because nobody gives you the graph. You only get a list of words, and you have to build the edges yourself by comparing them. The teacher's lessons: each pair of neighbouring words gives at most one rule "x comes before y", and only from the first letter where they differ. Those rules are directed edges. Kahn's algorithm (in-degree + queue) then prints the letters in order. She also lists the two ways the input can be invalid: a contradiction (a cycle) and the prefix trap ("abc" written before "ab").
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · Graph basics you need for this page
- Part A · Build the letter graph, then BFS topological sort (Kahn)
- Part B · Revision page
Part 0 · Graph basics
Nodes and edges
A graph is a set of points called nodes joined by edges. On this page the nodes are letters, and an edge (b)→(a) will mean "b comes before a in the alien alphabet".
- Undirected edge
(x)—(y): works both ways. - Directed edge
(x)→(y): one way only. "b before a" does not mean "a before b", so our edges are directed. - Weighted edge: an edge with a number on it (a cost). Not needed here.
Storing a graph
| form | looks like | meaning |
|---|---|---|
| edge list | [(1,0),(3,0),(0,2)] | each pair is one arrow x → y |
| adjacency matrix | m[x][y] = 1 | an n × n table of "is there an arrow?" |
| adjacency list | adj[x] = [y, …] | for each node, the list of nodes it points to (its children) |
n = 4
edges = [(1, 0), (3, 0), (0, 2), (1, 3)]
adj = [[] for _ in range(n)]
for x, y in edges:
adj[x].append(y) # directed: only x -> y
print(adj) # [[2], [0, 3], [], [0]]Letters as node numbers
There are only 26 lowercase letters, so we can number them 0 … 25: ord(ch) - ord('a'). For 'c' that's 99 − 97 = 2. Then plain lists of size 26 can hold the adjacency list, the in-degrees and a "present" flag. The teacher mentions the other option too: a dictionary keyed by the letter itself. Both work; the index way is simple and fast.
print(ord('c') - ord('a')) # 2
print(chr(2 + ord('a'))) # cVisited and connected components
Searches mark nodes as visited so they don't process a node twice or loop forever. A graph can be in several separate pieces (connected components), so we start searches from every suitable node, not just one. Here some letters may have no rules at all; they are tiny components of one node, and they still belong in the answer.
BFS and DFS
| BFS | DFS | |
|---|---|---|
| idea | level by level, using a queue (collections.deque) | deep first, using recursion or a stack |
| in topological sort | Kahn's algorithm: in-degree + queue (what the teacher uses) | finish order reversed (shown below as a bonus) |
Grid problems treat each cell as a node with 4 neighbours (up, down, left, right, with a bounds check). Not needed here.
In-degree and topological order
The in-degree of a node is the number of arrows coming into it. A topological order lists all the nodes so that every arrow points forward: for every x → y, x appears before y. That's exactly an alphabet: if b → a, then b comes before a.
Kahn's algorithm (BFS topological sort)
- Count every node's in-degree.
- Push the nodes with in-degree 0 into a queue (nothing must come before them).
- Pop a node, append it to the order, and reduce each child's in-degree by 1 (as if we deleted the arrow). If a child reaches 0, push it.
- If the order is shorter than the number of nodes, some nodes never reached 0. That only happens when there is a cycle: no valid order exists.
from collections import deque
def kahn(n, adj):
indeg = [0] * n
for x in range(n):
for y in adj[x]:
indeg[y] += 1
queue = deque(i for i in range(n) if indeg[i] == 0)
order = []
while queue:
x = queue.popleft()
order.append(x)
for y in adj[x]:
indeg[y] -= 1
if indeg[y] == 0:
queue.append(y)
return order if len(order) == n else [] # [] means a cycle
print(kahn(4, [[2], [0, 3], [], [0]])) # [1, 3, 0, 2]
print(kahn(2, [[1], [0]])) # []Bonus: topological order with DFS
DFS can also do it: after a node has finished all its children, put it on a list. Reversing that list gives a topological order. Cycles are caught with a "on the current path" mark (grey). The teacher uses BFS for this problem, so this is only for reference.
def dfs_topo(n, adj):
state = [0] * n # 0 new, 1 on path, 2 done
out = []
def dfs(x):
state[x] = 1
for y in adj[x]:
if state[y] == 1:
return False # back to the current path: cycle
if state[y] == 0 and not dfs(y):
return False
state[x] = 2
out.append(x) # all children finished
return True
for i in range(n):
if state[i] == 0 and not dfs(i):
return []
return out[::-1]
print(dfs_topo(4, [[2], [0, 3], [], [0]])) # [1, 3, 0, 2]Part A · Build the letter graph, then Kahn's algorithm
LeetCode 269 (premium) · also on GeeksforGeeks as "Alien Dictionary"
1The question in simple words
An alien language uses the same lowercase letters a–z, but in an unknown order. Maybe their alphabet starts with b, then d… We don't know.
You get a list of words words, and you are told it is sorted lexicographically by the alien order, meaning sorted the way a dictionary is sorted, but using the alien alphabet. Your job:
- Return a string with every distinct letter that appears in the words, in the alien order.
- If the words can't be sorted under any order (the input contradicts itself), return the empty string
"". - If several orders are possible, any one of them is accepted.
words = ["baa", "abcd", "abca", "cab", "cad"] answer: "bdac"
The very first pair already tells us something: "baa" is written before "abcd". Their first letters differ (b vs a), so in this language b comes before a. Strange for us, normal for them.
true. Should I return True?→ No. The judge runs your returned string through its own checker (because many answers can be correct) and prints
true if your order is valid. Your function still returns the string of letters, or "". The teacher warns: don't look at the displayed output and blindly return True/False.2What the constraints tell us
- The total number of characters over all words is up to about 10⁶. The teacher counts this for the example: 3 + 4 + 4 + 3 + 3 = 17 characters. So we must read each character only a few times: O(total characters).
- But there are only 26 possible letters. However long the input is, the graph has at most 26 nodes and at most 26 × 25 edges. So the graph part is tiny, almost constant.
- Letters are lowercase a–z → we can use list indices 0 … 25 instead of a dictionary.
3Intuition: how a real dictionary is ordered
The teacher asks: how do you find "aeronautics" in an English dictionary? First you jump to the a section. Inside it you don't look at "az…"; you look at "ae…". Then "aer…", and so on. Words are ordered by the first letter; when the first letters are equal, by the second; and so on.
Turn that round. If two neighbouring words in a sorted dictionary first differ at position j, then the letter of the first word at j comes before the letter of the second word at j. That's one rule, and it's a directed edge in a graph of letters. Collect all the rules, and the alphabet is any order that respects every arrow, which is a topological order.
4Building the logic from the teacher's examples
Rule 1: only the FIRST different letter tells you anything
Compare "air" and "bean" in English. "air" comes first, and the first letters differ: a before b ✓. Can we also say i comes before e (the second letters)? No. In English, e actually comes before i. "air" is first only because of a vs b. After the first difference, the remaining letters say nothing. The teacher's way of saying it: inside the a-words we have aa…, ab…, ac…, and inside the b-words ba…, bb…, …, so knowing one a-word and one b-word tells you nothing about their second letters.
w1[j] != w2[j], add the edge w1[j] → w2[j] and stop.Each neighbouring pair gives one edge
Why compare only neighbours (word i with word i + 1)? Because the list is sorted, any rule from a non-neighbour pair follows from the neighbour rules in between. With 5 words there are 4 pairs:
| pair | matching start | first different letters | rule (edge) |
|---|---|---|---|
| baa · abcd | — | j = 0: b vs a | b → a |
| abcd · abca | a, b, c | j = 3: d vs a | d → a |
| abca · cab | — | j = 0: a vs c | a → c |
| cab · cad | c, a | j = 2: b vs d | b → d |
The teacher pauses after the second pair: we know b → a and d → a, but nothing yet about b vs d. Both "b d a" and "d b a" fit so far. She waits for more information, and the last pair settles it: b → d. Putting it together: b, then d, then a, then c → "bdac".
The letter graph
(b) ─────────→ (a) ───→ (c)
╲ ↑
╲ │
──→ (d) ────┘
in-degree: b = 0 d = 1 a = 2 c = 1
Why a directed graph? "b before a" is not the same as "a before b". The arrow points from the earlier letter to the later one.
b has in-degree 0: no letter must come before it, so it's the smallest. That's the first letter of the answer. This is why topological sort fits: "sorted order" + "who comes before whom" = a topological order, and Kahn's BFS lets us print the letters one by one.
→ The teacher first writes "b is smaller than a" for understanding, then turns each one into a parent → child link: b has child a, d has child a, a has child c, b has child d. When BFS pops b, it needs to know "where can I go from b?", and the children list answers that.
Rule 2: a contradiction = a cycle → return ""
Add one more word at the end: […, "cad", "cbd"]. Compare "cad" and "cbd": c = c, then a vs b → edge a → b. But the first pair said b → a. One rule says b comes first, the other says a comes first. The teacher calls this a contradictory situation: no alphabet can satisfy both.
(b) ───→ (a)
↑ │
└────────┘ a cycle: b → a → b
In graph terms, a contradiction is a cycle. How do we notice it in Kahn's algorithm? Nodes on a cycle never reach in-degree 0, so they never enter the queue and never reach the answer. So: if the answer is shorter than the number of distinct letters, there was a cycle → return "". That's why we need to count the distinct letters.
→ The teacher considers this. Such a letter has in-degree 0, so it goes into the queue at the start and is printed. Any position for it is fine, since nothing restricts it. So it does not make the answer shorter. (In the code walk-through she says the length can fall short because of a cycle "or maybe letters without a relation". The second part isn't right: a letter without relations always gets printed. A shortfall happens only because of a cycle.)
Rule 3: the prefix trap ("abc" before "ab") → return ""
Compare "abc" and "ab". j = 0: a = a. j = 1: b = b. Now the shorter word has ended, and there was no mismatch. In every dictionary a word comes before any longer word that starts with it: "ab" must come before "abc". (The teacher's example: three a's must come before four a's.) So if the longer word is written first, the list can't be sorted under any alphabet → return "" right away.
| pair (w1, w2) | loop stops at | what we learn |
|---|---|---|
| "ab", "abc" | j = 2 = shorter length, no mismatch | fine, nothing learned (prefix first is correct) |
| "abc", "ab" | j = 2 = shorter length, no mismatch, len(w1) > len(w2) | invalid → return "" |
| "abc", "abc" | j = 3, equal lengths | fine, nothing learned |
| "abd", "abc" | j = 2, mismatch d vs c | edge d → c |
→ Beyond it, one of the words has no letter to compare. The teacher points out that "abc" (3 letters) and "abcd" (4 letters) can only be compared at positions 0, 1, 2. So we use
min_len = min(len(w1), len(w2)) and stop there.→ Because it gives no edge at all: there's no mismatching letter to make an arrow from. The graph alone would look fine, so this case must be caught separately, while comparing the pair.
Why the "present" list is needed
We keep in-degrees for all 26 letters, and they all start at 0. When we fill the queue, letters like f, g, h, which never appear in the words, also have in-degree 0. Without a check we would print them. So we keep present[c] = "this letter appears somewhere", and only push letters that are present and have in-degree 0. The same list helps count the distinct letters: we add 1 to the count only the first time a letter is seen (if it's already marked, skip, so repeats aren't counted).
Don't add the same edge twice
Different pairs can give the same rule again (e.g. "b vs a" found in two places). The teacher adds an edge only if it isn't already in the children list.
→ Nothing breaks, actually. The in-degree counts it twice, but popping the parent walks the children list, sees the child twice and reduces it twice, so the child is still freed while that same parent is being processed. The only visible effect: the order in which children get pushed can change, so you may get a different but still valid alphabet. Skipping duplicates keeps the lists small and the counts easy to read. (The test file checks that both versions always give a valid answer, and give "" in the same cases.)
5Approach steps
- Make
graph(26 empty lists),indegree(26 zeros) andpresent(26 False). - Go through every letter of every word: mark it present and count it the first time it's seen →
totaldistinct letters. - For each neighbouring pair (i, i + 1), with i from 0 to n − 2: find the first j where the letters differ, looking only up to the shorter length.
- No mismatch and the first word is longer → return
""(the prefix trap). - Mismatch → add edge
w1[j] → w2[j](if new) and add 1 to the in-degree ofw2[j].
- No mismatch and the first word is longer → return
- Push every letter that is present and has in-degree 0.
- Kahn's loop: pop, append the letter to the answer, reduce each child's in-degree, push it at 0.
- If the answer has fewer than
totalletters → cycle → return"". Otherwise return the answer.
6Code (Python)
from collections import deque
class Solution:
def alienOrder(self, words):
graph = [[] for _ in range(26)] # graph[x] = letters that come after x
indegree = [0] * 26
present = [False] * 26
total = 0 # number of distinct letters
for word in words:
for ch in word:
c = ord(ch) - ord('a')
if not present[c]: # count each letter only once
present[c] = True
total += 1
n = len(words)
for i in range(n - 1): # neighbouring pairs only
first = words[i]
second = words[i + 1]
min_len = min(len(first), len(second))
j = 0
while j < min_len and first[j] == second[j]:
j += 1
if j == min_len: # no mismatch found
if len(first) > len(second):
return "" # prefix trap: "abc" before "ab"
else:
u = ord(first[j]) - ord('a')
v = ord(second[j]) - ord('a')
if v not in graph[u]: # skip a rule we already have
graph[u].append(v)
indegree[v] += 1
queue = deque()
for c in range(26):
if present[c] and indegree[c] == 0:
queue.append(c)
result = []
while queue:
u = queue.popleft()
result.append(chr(u + ord('a')))
for v in graph[u]:
indegree[v] -= 1
if indegree[v] == 0:
queue.append(v)
if len(result) != total: # some letters stuck on a cycle
return ""
return "".join(result)On GeeksforGeeks the method is called findOrder(self, words); the body is the same.
7Code line by line
| line | what it means |
|---|---|
| graph = [[] for _ in range(26)] | One children list per letter (index 0 = 'a' … 25 = 'z'). |
| if not present[c]: present[c] = True total += 1 | The first time we meet a letter, mark it and count it. Seeing it again changes nothing. |
| for i in range(n - 1): | Pairs (0,1), (1,2), … (n−2, n−1). 5 words → 4 pairs. Using n − 1 stops words[i + 1] from going out of range. |
| min_len = min(len(first), len(second)) | We can only compare positions where both words have a letter. |
| while j < min_len and first[j] == second[j]: j += 1 | Skip the common start. The loop stops at the first mismatch, or at min_len if there isn't one. |
| if j == min_len: if len(first) > len(second): return "" | One word is a prefix of the other. That's fine if the shorter one comes first; if the longer one comes first, no alphabet can explain it. |
| u = ord(first[j]) - ord('a') v = ord(second[j]) - ord('a') | The two mismatching letters as indices. u comes before v. |
| if v not in graph[u]: graph[u].append(v) indegree[v] += 1 | Store the rule u → v once, and count one more letter that must come before v. |
| if present[c] and indegree[c] == 0: | Start with the real letters that nothing has to precede. |
| result.append(chr(u + ord('a'))) | The popping order is the alien alphabet. Turn the index back into a letter. |
| indegree[v] -= 1 if indegree[v] == 0: ... | u is placed, so v has one fewer letter waiting before it. At 0 it's free to be placed. |
| if len(result) != total: return "" | Some letters never got free → a cycle → contradiction. Return the empty string, not False. |
| return "".join(result) | The list of letters as one string (the return type must be a string). |
8Dry run
words = ["baa", "abcd", "abca", "cab", "cad"]. Distinct letters: b, a, c, d → total = 4. Edges from the pair table in step 4: b → a, d → a, a → c, b → d.
| letter | children | in-degree |
|---|---|---|
| a | c | 2 (from b, d) |
| b | a, d | 0 |
| c | — | 1 (from a) |
| d | a | 1 (from b) |
Fill the queue by scanning a … z: only b is present with in-degree 0 → queue = [b].
| step | popped | children checked | queue AFTER | in-degree after (a b c d) | answer so far |
|---|---|---|---|---|---|
| 0 | — | — | [b] | 2 0 1 1 | "" |
| 1 | b | a: 2→1 · d: 1→0 push | [d] | 1 0 1 0 | "b" |
| 2 | d | a: 1→0 push | [a] | 0 0 1 0 | "bd" |
| 3 | a | c: 1→0 push | [c] | 0 0 0 0 | "bda" |
| 4 | c | (none) | [ ] | 0 0 0 0 | "bdac" |
Length 4 = total 4 → return "bdac" ✓
(b)✓ ───→ (a) ───→ (c)
╲ ↑
→ (d) ─┘
a still waits for d (b)✓ ───→ (a) ───→ (c)
╲ ↑
→ (d)✓─┘
a is now freeDry run of the contradiction
["baa", "abcd", "abca", "cab", "cad", "cbd"] adds edge a → b. In-degrees: a 2, b 1, c 1, d 1. No present letter has in-degree 0, so the queue starts empty, the answer stays "" (length 0 ≠ 4) → return "".
Dry run of the prefix trap
["abc", "ab"]: min_len = 2, j goes 0 → 1 → 2 with no mismatch. j == min_len and 3 > 2 → return "" before any BFS.
9Complexity & remember
- Time O(C), where C = total characters (up to 10⁶). Marking letters reads each character once. Comparing pairs reads each word at most twice. The BFS works on at most 26 letters and their edges. The teacher stresses that the for loop inside the BFS doesn't run "26 times for each letter": over the whole BFS it runs once per edge in total (b has 2 children, d has 1, a has 1). So the BFS is O(26 + E), which is tiny next to 10⁶.
- Space O(1): graph, in-degree, present and the queue are all sized by the 26 letters, not by the input. In the example there are 17 characters but only 4 letters ever go into the queue. (
v not in graph[u]scans at most 25 entries, also constant.)
The teacher's closing advice: the hard part is breaking the problem into rules and remembering the two failure conditions. Re-watch and revise them every few days, or as a beginner you'll forget them within a week.
Part B · Revision page
| situation for a pair (w1, w2) | what to do |
|---|---|
| first mismatch at j | edge w1[j] → w2[j], in-degree of w2[j] + 1 (once per distinct edge) |
| no mismatch, len(w1) ≤ len(w2) | nothing learned, go on |
| no mismatch, len(w1) > len(w2) | return "" (prefix trap) |
| letters after the first mismatch | ignore them: they tell you nothing |
| Course Schedule II | Alien Dictionary | |
|---|---|---|
| nodes | courses 0 … n−1 | the letters present (out of 26) |
| edges | given as prerequisite pairs | built from neighbouring words |
| start queue with | in-degree 0 | present and in-degree 0 |
| cycle check | order length < n | answer length < distinct letters |
| extra invalid case | — | prefix trap |
| on failure return | [] | "" |
2. Compare neighbouring words only; the first mismatch gives one directed edge.
3. Longer word before its own prefix ("abc" before "ab") → return "".
4. Kahn's BFS: push letters that are present with in-degree 0; pop order = alphabet.
5. Answer shorter than the number of distinct letters → cycle → return "".
✗ forgetting the prefix trap (it gives no edge, so the cycle check can't catch it)
✗ pushing letters that never appear (check
present)✗ counting a letter every time it appears instead of once
✗ returning True/False because the judge shows
true✗ comparing past the shorter word's length (index error)
s = Solution() print(s.alienOrder(["baa", "abcd", "abca", "cab", "cad"])) # bdac print(repr(s.alienOrder(["baa", "abcd", "abca", "cab", "cad", "cbd"]))) # '' print(repr(s.alienOrder(["abc", "ab"]))) # '' print(s.alienOrder(["ab", "abc"])) # abc print(s.alienOrder(["wrt", "wrf", "er", "ett", "rftt"])) # wertf print(s.alienOrder(["z", "x"])) # zx print(repr(s.alienOrder(["z", "x", "z"]))) # ''
Based on this video: Alien Dictionary | Topological Sort (BFS)