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

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".

Storing a graph

formlooks likemeaning
edge list[(1,0),(3,0),(0,2)]each pair is one arrow x → y
adjacency matrixm[x][y] = 1an n × n table of "is there an arrow?"
adjacency listadj[x] = [y, …]for each node, the list of nodes it points to (its children)
edge list → adjacency list (directed)
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.

letter ↔ index
print(ord('c') - ord('a'))       # 2
print(chr(2 + ord('a')))         # c

Visited 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

BFSDFS
idealevel by level, using a queue (collections.deque)deep first, using recursion or a stack
in topological sortKahn'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)

  1. Count every node's in-degree.
  2. Push the nodes with in-degree 0 into a queue (nothing must come before them).
  3. 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.
  4. 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.
Kahn's algorithm template
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.

DFS topological order template
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:

The teacher's example
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.

Doubt 1: on the practice site the expected output shows 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

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.

Rule 1For neighbouring words w1, w2: walk j = 0, 1, 2… while the letters are equal. At the first j where 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:

pairmatching startfirst different lettersrule (edge)
baa · abcd—j = 0: b vs ab → a
abcd · abcaa, b, cj = 3: d vs ad → a
abca · cab—j = 0: a vs ca → c
cab · cadc, aj = 2: b vs db → 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.

Doubt 2: why keep children lists and not just write "b < a" somewhere?
→ 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.

Doubt 3: what about a letter that appears in the words but never gets any rule, like an extra "e"?
→ 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 atwhat we learn
"ab", "abc"j = 2 = shorter length, no mismatchfine, 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 lengthsfine, nothing learned
"abd", "abc"j = 2, mismatch d vs cedge d → c
Doubt 4: why compare only up to the shorter length?
→ 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.
Doubt 5: why does the prefix trap not show up as a cycle?
→ 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.

Doubt 6: what would go wrong if we stored the duplicate?
→ 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

  1. Make graph (26 empty lists), indegree (26 zeros) and present (26 False).
  2. Go through every letter of every word: mark it present and count it the first time it's seen → total distinct letters.
  3. 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 of w2[j].
  4. Push every letter that is present and has in-degree 0.
  5. Kahn's loop: pop, append the letter to the answer, reduce each child's in-degree, push it at 0.
  6. If the answer has fewer than total letters → cycle → return "". Otherwise return the answer.

6Code (Python)

Alien Dictionary, build edges + Kahn's BFS
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

linewhat 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 += 1The 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 += 1Skip 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] += 1Store 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.

letterchildrenin-degree
ac2 (from b, d)
ba, d0
c—1 (from a)
da1 (from b)

Fill the queue by scanning a … z: only b is present with in-degree 0 → queue = [b].

steppoppedchildren checkedqueue AFTERin-degree after (a b c d)answer so far
0——[b]2 0 1 1""
1ba: 2→1 · d: 1→0 push[d]1 0 1 0"b"
2da: 1→0 push[a]0 0 1 0"bd"
3ac: 1→0 push[c]0 0 0 0"bda"
4c(none)[ ]0 0 0 0"bdac"

Length 4 = total 4 → return "bdac" ✓

after step 1 (✓ = placed)
  (b)✓ ───→ (a) ───→ (c)
     ╲       ↑
      → (d) ─┘
  a still waits for d
after step 2
  (b)✓ ───→ (a) ───→ (c)
     ╲       ↑
      → (d)✓─┘
  a is now free

Dry 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

Remember Alien DictionaryNeighbouring words → first mismatch → one edge w1[j] → w2[j]. No mismatch and the longer word comes first → "". Then Kahn's BFS over present letters. Fewer letters out than distinct letters in → cycle → "".

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 jedge 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 mismatchignore them: they tell you nothing
Course Schedule IIAlien Dictionary
nodescourses 0 … n−1the letters present (out of 26)
edgesgiven as prerequisite pairsbuilt from neighbouring words
start queue within-degree 0present and in-degree 0
cycle checkorder length < nanswer length < distinct letters
extra invalid case—prefix trap
on failure return[]""
If you remember only 5 lines 1. Sorted words + "who comes before whom" → topological sort on letters.
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 "".
Mistakes to avoid ✗ adding edges for letters after the first mismatch
✗ 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)
test it yourself (paste under the solution above)
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)