DSA DSA In Depth
0%
lesson aho-corasickmulti-patternautomata

Aho-Corasick Automaton: Trie + BFS Failure Links + Dictionary Suffix Links

7 min read Module 4.1: Tries, Exact String Matching & Multi-Pattern Automata Updated Oct 23, 2026
📄 Cheat Sheet
Aho-Corasick Algorithm for Multi-Pattern String Matching
Pavel Mavrin Aho-Corasick Algorithm for Multi-Pattern String Matching Watch on YouTube ↗

Pavel Mavrin constructs the Aho-Corasick trie, BFS failure links, and completed DFA transition table step-by-step.

0:00 / --:--

1. The Three Structural Pillars of Aho-Corasick

Given a dictionary $\mathcal{P} = \{P_1, P_2, \dots, P_K\}$ of total length $M = \sum_{i=1}^K |P_i|$, we first insert all $P_i$ into a standard rooted Trie where the root is state $0$. Each node $u$ corresponds to a unique prefix string $S(u)$ of one or more patterns. We augment the Trie with three structures computed in a single Breadth-First Search (BFS) over the nodes by depth:

  1. Failure Link ($\text{fail}[u]$): The unique Trie node $v \neq u$ maximizing $|S(v)|$ such that $S(v)$ is a proper suffix of $S(u)$. For the root and all depth-1 children of the root, $\text{fail}[u] = 0$.
  2. Completed DFA Transition Table ($\text{go}[u][c]$): Instead of walking up failure links at query time with a while loop (which adds branch mispredictions and requires amortized analysis), we materialize the full transition table during BFS: $$\text{go}[u][c] = \begin{cases} \text{trie}[u][c] & \text{if child } c \text{ exists at } u \\ 0 & \text{if } u = 0 \text{ and child } c \text{ is absent} \\ \text{go}[\text{fail}[u]][c] & \text{if } u > 0 \text{ and child } c \text{ is absent} \end{cases}$$ Because $\text{depth}(\text{fail}[u]) < \text{depth}(u)$, when BFS visits node $u$, $\text{go}[\text{fail}[u]][c]$ has already been completely computed for all characters $c \in \Sigma$!
  3. Dictionary Suffix Link ($\text{exit}[u]$ / Output Link): A pattern $P_j$ might be a proper substring of another pattern (e.g., $\texttt{"she"}$ and $\texttt{"he"}$ inside $\texttt{"ushers"}$). When the automaton reaches state $u = \texttt{"she"}$, we must also report $\texttt{"he"}$ at $\text{fail}[u]$. However, walking the entire $\text{fail}$ chain at every text position takes $O(\text{depth}(u))$ even when no pattern ends on that chain, degrading worst-case runtime to $O(N \cdot \max |P_i|)$. We fix this by computing $\text{exit}[u]$ during BFS: $$\text{exit}[u] = \begin{cases} \text{fail}[u] & \text{if } \text{is\_terminal}[\text{fail}[u]] = \text{true} \\ \text{exit}[\text{fail}[u]] & \text{otherwise} \end{cases}$$ Traversing $\text{exit}[u]$ jumps exclusively between terminal states that actually emit matches, guaranteeing $O(1)$ work per reported match $Z$.
Automaton Variant Construction Time Memory Footprint Text Scan Time (for $Z$ matches)
Lazy Failure Chain + Raw Fail Output Walk $O(M)$ $O(M)$ (hash/list edges) $O(N \cdot \max |P_i|)$ (degrades on empty fail chains!)
Lazy Failure Chain + Compressed $\text{exit}[u]$ Links $O(M \log |\Sigma|)$ $O(M)$ $O(N \log |\Sigma| + Z)$ amortized
Completed DFA ($\text{go}[u][c]$) + $\text{exit}[u]$ Links $O(M \cdot |\Sigma|)$ $O(M \cdot |\Sigma|)$ $O(N + Z)$ worst-case ($1$ table lookup / byte)
Aho-Corasick Longest-Suffix State Invariant

After processing prefix $T[0..i]$ through $u_{i+1} = \text{go}[u_i][T[i]]$ starting from $u_0 = 0$, the active state $u_{i+1}$ is the unique node in the Trie whose path string $S(u_{i+1})$ is the longest suffix of $T[0..i]$ that appears as a prefix of at least one pattern in $\mathcal{P}$. Every pattern $P_k \in \mathcal{P}$ ending at index $i$ corresponds bijectively to a terminal node reachable from $u_{i+1}$ via zero or more $\text{exit}[\cdot]$ hops.

Contiguous Flat Arena vs. Pointer-Based Nodes

Representing each Aho-Corasick state as a struct inside a single contiguous std::vector<Node> arena indexed by int32_t state IDs ensures that go[u][c] is a single base+offset calculation arena[u].go[c]. For ASCII alphabets ($|\Sigma| = 256$), a node with int32_t go[256] occupies 1 KB; if only lowercase English letters ($|\Sigma| = 26$) are needed, int32_t go[26] + fail + exit fits inside 128 bytes (exactly two 64-byte cache lines), allowing hot states near the root to stay permanently resident in L1d cache.

2. Visual Trace: Dictionary $\{\texttt{"he"}, \texttt{"she"}, \texttt{"his"}, \texttt{"hers"}\}$

In the classic Aho-Corasick paper example below, solid arrows show Trie edges, dashed red arrows show non-root failure links $\text{fail}[u]$, and green double-bordered circles denote terminal states. Notice that state $5$ ($\texttt{"she"}$) has $\text{fail}[5] = 2$ ($\texttt{"he"}$), which is itself terminal, so $\text{exit}[5] = 2$: matching $\texttt{"she"}$ immediately emits both $\texttt{"she"}$ and $\texttt{"he"}$.

0:ε h 1:h e 2:he r 8:her s 9:hers i 6:hi s 7:his s 3:s h 4:sh e 5:she --- fail[u] (longest suffix) === Terminal state (exit[5] = 2)
flowchart TD A["Insert all patterns P_1..P_K into Trie (root = 0)"] --> B["Init BFS Queue with existing depth-1 children of root (fail = 0)"] B --> C{"Queue empty?"} C -- "No: pop u" --> D["Compute exit[u] = is_term[fail[u]] ? fail[u] : exit[fail[u]]"] D --> E["For each symbol c in Sigma"] E --> F{"Child v = trie[u][c] exists?"} F -- "Yes" --> G["fail[v] = go[fail[u]][c]; push v to Queue"] F -- "No" --> H["Wire shortcut go[u][c] = go[fail[u]][c]"] G --> C H --> C C -- "Yes" --> I["DFA Ready: Scan T in O(N + Z) time"]
Aho-Corasick Multi-Pattern DFA (Failure + Output Links) & Manacher / Eertree
O(N + ∑|P_i| + Z) Multi-Pattern · O(N) Palindromes

Scan text T = "ushers" in a single pass against dictionary {"he", "she", "his", "hers"} using BFS failure links and output links, or trace Manacher’s O(N) palindrome radius mirroring & Eertree dual roots.

Step 1 / 6
Custom Input:
Timeline

Invariant & State

Active Trie State = root ("") | Failure Suffix Link fail(u) = root | Emitted Matches = ∅
🌳 Live 2D Hierarchical Tree Topology — Aho-Corasick Multi-Pattern DFA (Failure + Output Links) & Manacher / Eertree Visited/Active Pivot/Root Resolved
state=ro u [0] s [1] h [2] e [3] r [4] s [5]
[0]ustate=root ("")
[1]s
[2]h
[3]e
[4]r
[5]s
Text Index i
i = 0 ('u')
Automaton State
root ("")
fail(state)
root
Output Matches
∅
[Step 1] Read "u": no "u" edge from root -> stay at root.

3. Production Implementation: Completed-DFA Aho-Corasick

#include <array>
#include <queue>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

class AhoCorasick {
public:
    static constexpr int ALPHA = 26;
    struct Node {
        std::array<int, ALPHA> go{};
        int fail = 0;
        int exit_link = 0;      // Nearest terminal ancestor on fail chain
        std::vector<int> pat_ids; // Pattern IDs ending exactly at this node
    };

    AhoCorasick() { nodes_.emplace_back(); }

    void insert(std::string_view pat, int id) {
        int u = 0;
        for (char ch : pat) {
            int c = ch - 'a';
            if (nodes_[u].go[c] == 0) {
                nodes_[u].go[c] = static_cast<int>(nodes_.size());
                nodes_.emplace_back();
            }
            u = nodes_[u].go[c];
        }
        nodes_[u].pat_ids.push_back(id);
    }

    void build() {
        std::queue<int> q;
        for (int c = 0; c < ALPHA; ++c) {
            int v = nodes_[0].go[c];
            if (v != 0) {
                nodes_[v].fail = 0;
                q.push(v);
            }
        }
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            int f = nodes_[u].fail;
            nodes_[u].exit_link = !nodes_[f].pat_ids.empty() ? f : nodes_[f].exit_link;

            for (int c = 0; c < ALPHA; ++c) {
                int v = nodes_[u].go[c];
                if (v != 0) {
                    nodes_[v].fail = nodes_[f].go[c];
                    q.push(v);
                } else {
                    nodes_[u].go[c] = nodes_[f].go[c];
                }
            }
        }
    }

    // Returns (end_index_in_text, pattern_id) for all occurrences in O(N + Z)
    std::vector<std::pair<int, int>> search(std::string_view text) const {
        std::vector<std::pair<int, int>> matches;
        int u = 0;
        for (int i = 0; i < static_cast<int>(text.size()); ++i) {
            u = nodes_[u].go[text[i] - 'a'];
            for (int cur = u; cur != 0; cur = nodes_[cur].exit_link) {
                for (int pid : nodes_[cur].pat_ids) {
                    matches.emplace_back(i, pid);
                }
            }
        }
        return matches;
    }

private:
    std::vector<Node> nodes_;
};
Never Walk Raw Failure Links to Collect Matches

A common implementation bug is writing for (int cur = u; cur != 0; cur = nodes_[cur].fail) inside the text loop to emit matches. Consider a dictionary containing a single long pattern $\texttt{"aaaaaaaaaa...ab"}$ of length $M$ and a text $T = \texttt{"aaaaaaaaaa...a"}$ of length $N$. At every character, $u$ sits at depth $M - 1$ and the raw fail chain steps through $M - 1$ non-terminal nodes before reaching root $0$, turning an $O(N + Z)$ algorithm with $Z = 0$ matches into $\Theta(N \cdot M)$ time! Always compress the output chain via $\text{exit\_link}$ (or propagate pattern counts if only counting total matches).

Check yourself Question 1 of 4
Concept Check Why Aho-Corasick Failure Links Require Breadth-First Search (BFS) recall

In KMP for a single pattern, we compute $\pi[i]$ in a simple left-to-right loop (which is a Depth-First traversal of the single branch). In Aho-Corasick for multiple patterns, however, we MUST compute $\text{fail}[u]$ and $\text{go}[u][c]$ in Breadth-First Search (level-order) order rather than Depth-First Search (DFS) order.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

How can you compute the exact occurrence count of every pattern $P_1, \dots, P_K$ in strict $O(N + M)$ time, independent of $Z$?

Key Takeaways

Aho-Corasick augments a multi-pattern Trie with BFS failure links fail[u], completed DFA transitions go[u][c], and dictionary suffix links exit[u] to match K patterns in O(N + M + Z) time.
Because fail[u] always has strictly smaller depth than u, computing go[u][c] = go[fail[u]][c] in Breadth-First Search order resolves all failure chains into O(1) table lookups without recursion.
Dictionary suffix links exit[u] skip over non-terminal states on the failure path so reporting Z overlapping matches takes O(Z) work instead of O(N * max|P_i|).
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue