Aho–Corasick
Search a text for every word of a dictionary simultaneously by walking a trie augmented with KMP-style failure links.
Overview
Aho–Corasick finds all occurrences of all k patterns in a text in O(n + total pattern length + number of matches). It generalises Knuth–Morris–Pratt (KMP) from one pattern to a dictionary: the patterns are stored in a Trie, and every trie node gets a failure link pointing to the longest proper suffix of its string that is also a prefix of some pattern — exactly what the LPS array does for a single string.
The result is a deterministic automaton. Reading the text one character at a time, the current node always represents the longest suffix of the text read so far that is a prefix of some pattern. Whenever that node (or any node reachable by failure links, via output links) marks the end of a pattern, a match is reported.
Intuition
A mental model before the formal terms.
Lay out all the patterns as branching paths from a common root. Walking the text down these paths is easy until a character has no branch. Instead of returning to the root and re-reading, you jump to the deepest other path that ends with the same characters you have just read — the failure link. It is the same slide-without-forgetting idea as KMP, except the "pattern" is a whole tree.
Output links solve a subtlety: after reading "she" you are at the node for "she", but "he" also ends here. The failure chain from "she" passes through "he", so following it (or a precomputed shortcut to the nearest pattern node) reports every pattern that is a suffix of the current position.
How it works
- Insert every pattern into a trie; mark terminal nodes with the pattern index.
- Compute failure links in BFS order from the root. For the root's children,
fail = root. For a nodevreached fromuby characterc: followu.fail,u.fail.fail, … until a node with a child oncexists (or the root);v.failis that child (or the root). - Set
v.output = v.failifv.failis terminal, otherwisev.output = v.fail.output— a shortcut to the nearest terminal node on the failure chain. - Scan the text: for each character
c, follow failure links from the current node until a child oncexists, then move to it. Report the current node if terminal, then walk its output chain reporting every terminal node. - Optionally precompute full
gototransitions for every (node, character) so each text character costs exactly one table lookup.
Why it works
Invariant: after reading t[0..i], the current node is the longest suffix of t[0..i] that is a prefix of some pattern. Adding one character either extends that suffix (child exists) or must shorten it; the failure link gives the next-longest candidate, and the BFS construction guarantees every failure link is the longest such suffix, so no shorter valid state is skipped.
Every pattern occurrence ending at i is a suffix of t[0..i] and a prefix of itself, so it lies on the failure chain of the current node; the output links enumerate exactly those chain nodes that are terminal.
Amortised bound: depth of the current node increases by at most 1 per character and each failure step decreases it, so failure steps over the whole text are at most n. Construction is O(total length · σ) with full transitions or O(total length) amortised with lazy failure walking.
Recognition
How to tell a problem wants this.
- Search a text for many words at once: spam filters, banned-word detection, "count how many dictionary words appear in the string".
- The naive approach is "run KMP once per pattern", and
k · nis too large. - DP over an automaton: "count strings of length
Lthat avoid all forbidden substrings" — the AC automaton is the state space.
Interactive visualization
Play, step, change the input. ← → and space work too.
Showing the closely related Trie visualization.
1node = root2for ch in word:3 if ch not in node.children: (insert) create child / (search) return false4 node = node.children[ch]5insert: node.end = true6search: return node.end7startsWith: return truePseudocode
1build trie of all patterns2queue = children of root, their fail = root3while queue: u = pop; for (c, v) in u.children:4 f = u.fail; while f != root and c not in f.children: f = f.fail5 v.fail = f.children[c] if c in f.children and f.children[c] != v else root6 v.output = v.fail if v.fail terminal else v.fail.output; push v7node = root; for i, c in text:8 while node != root and c not in node.children: node = node.fail9 node = node.children.get(c, root)10 for w in node, node.output, node.output.output, …: report (i, w.pattern)Implementations
1from collections import deque2 3 4class AhoCorasick:5 """A trie of all patterns plus "failure" links that point to the longest6 proper suffix which is also a prefix of some pattern. One pass over the7 text finds every occurrence of every pattern, in O(text + total + hits)."""8 91 · One node per trie position; fail is the suffix link, out the matches10 def __init__(self) -> None:11 self.next: list[dict[str, int]] = [{}]12 self.fail: list[int] = [0]13 self.out: list[list[int]] = [[]] # ids of patterns ending here14 self.out_link: list[int] = [-1] # next node up the fail chain with output15 16 def _new_node(self) -> int:17 self.next.append({})18 self.fail.append(0)19 self.out.append([])20 self.out_link.append(-1)21 return len(self.next) - 122 232 · Insert every pattern into the trie, recording its id at the end24 def add(self, pattern: str, ident: int) -> None:25 cur = 026 for c in pattern:27 nxt = self.next[cur].get(c)28 if nxt is None:29 nxt = self._new_node()30 self.next[cur][c] = nxt31 cur = nxt32 self.out[cur].append(ident)33 343 · BFS builds failure links: a node's fail is its parent's fail + c35 def build(self) -> None:36 q = deque()37 for child in self.next[0].values():38 self.fail[child] = 039 q.append(child)40 while q:41 u = q.popleft()42 f = self.fail[u]43 # out_link skips straight to the next ancestor that actually matches44 self.out_link[u] = f if self.out[f] else self.out_link[f]45 for c, v in self.next[u].items():46 f2 = self.fail[u]47 while f2 != 0 and c not in self.next[f2]:48 f2 = self.fail[f2]49 cand = self.next[f2].get(c)50 self.fail[v] = cand if cand is not None and cand != v else 051 q.append(v)52 534 · Walk the text once; on a mismatch follow fail links, never backing up54 def search(self, text: str) -> list[tuple[int, int]]:55 hits: list[tuple[int, int]] = [] # (end index, pattern id)56 cur = 057 for i, c in enumerate(text):58 while cur != 0 and c not in self.next[cur]:59 cur = self.fail[cur]60 cur = self.next[cur].get(c, 0)61 625 · Report this node's patterns, then follow output links for the rest63 node = cur64 while node != -1:65 for ident in self.out[node]:66 hits.append((i, ident))67 if node == 0:68 break69 node = self.out_link[node]70 return hits- This version uses four *parallel lists* (
next,fail,out,out_link) rather than a node class, which avoids one Python object per node and is measurably lighter for large automata. self.next[cur].get(c)returnsNonefor a missing transition, andis Noneis the correct test — a node index of 0 is falsy and would break a truthiness check.self.next[cur].get(c, 0)insearchsupplies the root as the default target, which is the fall-back-to-root behaviour.for i, c in enumerate(text)iterates index and character together; Python strings iterate by code point, so no surrogate handling is needed.- The
out_linkchain is walked with an explicitwhilebecause Python has no C-styleforwith three clauses.
- Parallel lists beat a per-node class in CPython: each object otherwise costs a
__dict__(or__slots__overhead) plus a reference, which adds up over hundreds of thousands of nodes. dict.get(key)returningNoneversusdict.get(key, 0)is the difference between "is there a transition" and "where do I go" — both spellings appear here deliberately.collections.dequeis the right BFS queue;list.pop(0)would be O(n).pyahocorasickis the established C extension if this is needed in production.
- Testing
if self.next[cur].get(c):instead ofis None, which treats node index 0 as missing. - Using a
listwithpop(0)for the BFS queue. - Building failure links depth-first, so a parent's link is not final when its child is processed.
- The falsy-zero trap appears in three languages at once and in different disguises:
||versus??in JS/TS, and truthiness versusis Nonein Python. Node index 0 is the root, so "missing" and "goes to 0" must stay distinguishable. - Node storage: C++ and JS/TS use an array of node records, while the Python version uses parallel lists to dodge per-object overhead — the same structure, a different memory trade.
- Reference stability: only C++ has to worry that
push_backinvalidates a heldNode&; JS/TS objects and Python list elements are separately allocated. - String iteration: Python and JS/TS
for...ofwalk code points, while acharCodeAtor C++charloop walks units and bytes — the automaton is correct either way, but the pattern and the text must agree.
Complexity
L = total pattern length, z = number of reported matches, σ = alphabet size (for full transition tables).
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Matching a dictionary of many words against one or many texts.
- Counting or locating occurrences of each of
kpatterns in a single pass. - Automaton DP: counting strings that contain/avoid a set of substrings, or "minimum edits to remove all forbidden words".
- A single pattern — Knuth–Morris–Pratt (KMP) is simpler with the same bound.
- Patterns change frequently — the automaton must be rebuilt; a Suffix Array of the text answers ad-hoc queries instead.
- Huge alphabets with full transition tables — memory
O(L·σ)explodes; use lazy failure walking or hash-map children.
Alternatives
Common mistakes
- Computing failure links with DFS instead of BFS — a node's failure link depends on shallower nodes being finished.
- Setting
v.fail = vwhen the failure walk from the root lands back onvitself (happens for depth-1 nodes). - Reporting only the current node and forgetting output links, missing patterns that are suffixes of other patterns (
"he"inside"she"). - Reporting end indices but returning them as start indices; the start is
end - len(pattern) + 1.
Interview patterns
- Word Search II can be solved with a trie; Aho–Corasick is the follow-up for "find all dictionary words in a long text".
- Stream checker: "has any word in the dictionary just been completed by the stream?"
- Count occurrences of each keyword in a document.
- Forbidden-substring DP with the automaton as state.
- Course ScheduleIntermediate
- Network Delay TimeAdvanced
- Number of IslandsIntermediate