Algorithm Search⌘K

Proofs: Aho-Corasick algorithm

These are the human proofs of Aho-Corasick algorithm, at full length.

Aho-Corasick outputs each occurrence of each pattern exactly once full

This proof shows: correctness, termination.

Claim. For a text T[0…n−1]T[0 \ldots n-1] and patterns P0,…,Pk−1P_0, \ldots, P_{k-1}, AHO-CORASICK stops and outputs each pair (s,j)(s, j) with 0≤s≤n−∣Pj∣0 \le s \le n - |P_j| and T[s…s+∣Pj∣−1]=PjT[s \ldots s + |P_j| - 1] = P_j exactly once, and outputs no other pair.

Setup. Let NN be the set of all prefixes of the patterns, including the empty string. After lines L1 to L3, the trie has one node for each string of NN, str(root)\mathrm{str}(\mathrm{root}) is the empty string, goto(v,a)\mathrm{goto}(v, a) is defined exactly when str(v)a∈N\mathrm{str}(v)a \in N, and out[v]\mathrm{out}[v] is the list of the indices jj with Pj=str(v)P_j = \mathrm{str}(v), each index once. We call vv a pattern node when out[v]≠∅\mathrm{out}[v] \ne \emptyset. The set NN is closed under prefixes: if ya∈Nya \in N, then y∈Ny \in N. We write ∣v∣|v| for the depth ∣str(v)∣|\mathrm{str}(v)|. For two suffixes of one string, the shorter is a suffix of the longer.

Lemma 1 (failure links). For each node u≠rootu \ne \mathrm{root}, fail[u]\mathrm{fail}[u] is the node of the longest proper suffix of str(u)\mathrm{str}(u) that lies in NN. Moreover, the nodes whose strings are proper suffixes of str(u)\mathrm{str}(u) are exactly fail[u],fail2[u],…\mathrm{fail}[u], \mathrm{fail}^2[u], \ldots, ending at the root, in decreasing order of depth.

Proof. By induction on ∣u∣|u|. The breadth-first order of lines L4 to L13 dequeues all nodes of depth dd before any node of depth d+1d + 1, and the link of a node uu of depth d+1≥2d + 1 \ge 2 is set when its parent, of depth dd, is dequeued. So when fail[u]\mathrm{fail}[u] is set, the links of all nodes of depth at most dd are already set.

Depth 1. The only proper suffix of a one-character string is the empty string, and line L4 sets fail[u]=root\mathrm{fail}[u] = \mathrm{root}. The chain is the root alone.

Depth d+1≥2d + 1 \ge 2. Let u=goto(v,a)u = \mathrm{goto}(v, a), so str(u)=str(v)a\mathrm{str}(u) = \mathrm{str}(v)a and ∣v∣=d≥1|v| = d \ge 1. A nonempty proper suffix of str(v)a\mathrm{str}(v)a has the form yaya, where yy is a proper suffix of str(v)\mathrm{str}(v). If ya∈Nya \in N, then y∈Ny \in N. So the nonempty candidates are the strings yaya with yy a proper suffix of str(v)\mathrm{str}(v) in NN and goto(y,a)\mathrm{goto}(y, a) defined. By the induction hypothesis for vv, these strings yy are the chain fail[v],fail2[v],…,root\mathrm{fail}[v], \mathrm{fail}^2[v], \ldots, \mathrm{root}, in decreasing order of length. Lines L9 to L11 walk this chain from the start and stop at the first node ff with goto(f,a)\mathrm{goto}(f, a) defined, or at the root. If goto(f,a)\mathrm{goto}(f, a) is defined, it is the longest candidate; it is not uu itself, because ∣f∣<∣v∣|f| < |v|. If not, then ff is the root and no candidate exists, and the longest proper suffix in NN is the empty string. Line L12 sets fail[u]\mathrm{fail}[u] to the right node in both cases.

For the chain, let xx be a proper suffix of str(u)\mathrm{str}(u) in NN, and let w=fail[u]w = \mathrm{fail}[u]. By the first part, ∣x∣≤∣w∣|x| \le |w|. If ∣x∣=∣w∣|x| = |w|, then x=str(w)x = \mathrm{str}(w). If ∣x∣<∣w∣|x| < |w|, then xx is a proper suffix of str(w)\mathrm{str}(w), and ∣w∣≤d<d+1|w| \le d < d + 1, so by the induction hypothesis xx is on the chain of ww. Conversely, each node of the chain spells a proper suffix of str(u)\mathrm{str}(u), by transitivity of the suffix relation. Each link strictly lowers the depth, so the chain is in decreasing order of depth and ends at the root. □\square

Lemma 2 (dictionary links). For each node vv, dict[v]\mathrm{dict}[v] is the pattern node of the longest proper suffix of str(v)\mathrm{str}(v) that is a pattern, or nil if no proper suffix is a pattern. Hence the sequence dict[v],dict2[v],…\mathrm{dict}[v], \mathrm{dict}^2[v], \ldots (up to nil) lists all pattern nodes whose strings are proper suffixes of str(v)\mathrm{str}(v), in decreasing order of depth, each once.

Proof. For the root, dict\mathrm{dict} is nil, and the empty string has no proper suffix. For v≠rootv \ne \mathrm{root}, line L7 runs when vv is dequeued. Then fail[v]\mathrm{fail}[v] has smaller depth, so it is the root or a node dequeued before vv, and its dict\mathrm{dict} value is final. By Lemma 1, the proper suffixes of str(v)\mathrm{str}(v) in NN are str(fail[v])\mathrm{str}(\mathrm{fail}[v]) and the proper suffixes of str(fail[v])\mathrm{str}(\mathrm{fail}[v]) in NN. Each pattern is in NN. So the longest proper suffix of str(v)\mathrm{str}(v) that is a pattern is str(fail[v])\mathrm{str}(\mathrm{fail}[v]) if fail[v]\mathrm{fail}[v] is a pattern node, and else the longest proper suffix of str(fail[v])\mathrm{str}(\mathrm{fail}[v]) that is a pattern, which is dict[fail[v]]\mathrm{dict}[\mathrm{fail}[v]] by induction on depth. This is line L7. The second statement follows by applying the first one to dict[v]\mathrm{dict}[v], then to dict2[v]\mathrm{dict}^2[v], and so on; the depth strictly falls, so the sequence ends. □\square

Lemma 3 (REPORT). REPORT(v,e)(v, e) outputs the pair (e−∣Pj∣,j)(e - |P_j|, j) for each jj such that PjP_j is a suffix of str(v)\mathrm{str}(v), each pair once, and nothing else.

Proof. The patterns that are suffixes of str(v)\mathrm{str}(v) are str(v)\mathrm{str}(v) itself, if vv is a pattern node, and the patterns among its proper suffixes. Line L19 starts the walk at vv in the first case and at dict[v]\mathrm{dict}[v] in the second. If vv is a pattern node, dict[v]\mathrm{dict}[v] lists exactly the remaining ones (Lemma 2). So the loop of lines L20 to L22 visits each pattern node whose string is a suffix of str(v)\mathrm{str}(v) exactly once, and line L21 outputs each index stored at it. Each index jj is stored at exactly one node, the node of PjP_j. □\square

Lemma 4 (search invariant). For 0≤e≤n0 \le e \le n, let λe\lambda_e be the longest suffix of T[0…e−1]T[0 \ldots e-1] that lies in NN. When REPORT(v,e)(v, e) is called, str(v)=λe\mathrm{str}(v) = \lambda_e.

Proof. By induction on ee. For e=0e = 0, the text read is empty, λ0\lambda_0 is the empty string, and line L14 calls REPORT with the root.

Assume the claim for e=ie = i, with vv the node of λi\lambda_i, and let c=T[i]c = T[i]. A nonempty suffix of T[0…i]T[0 \ldots i] in NN has the form ycyc with yy a suffix of T[0…i−1]T[0 \ldots i-1] and y∈Ny \in N (prefix closure). Then ∣y∣≤∣λi∣|y| \le |\lambda_i|, so yy is a suffix of λi\lambda_i: it is λi\lambda_i or a proper suffix of it in NN, that is, vv or a node of the failure chain of vv (Lemma 1). Line L16 walks v,fail[v],…v, \mathrm{fail}[v], \ldots in decreasing order of depth, and stops at the first node with a child along cc, or at the root. If that node has the child, the child spells the longest suffix of T[0…i]T[0 \ldots i] in NN, and line L17 moves there. If not, the walk stopped at the root and no candidate yy exists, so λi+1\lambda_{i+1} is empty and line L17 sets vv to the root. Line L18 then calls REPORT with the node of λi+1\lambda_{i+1}. □\square

Termination. The loop of lines L10 and L11 and the loop of line L16 each lower the depth of a node by at least 1 at each pass, and depths are non-negative integers, so they stop. The loop of line L20 lowers the depth at each pass by Lemma 2. The other loops run over finite sets. So the algorithm stops.

Theorem. The claim holds.

Proof. Each call REPORT(v,e)(v, e) outputs pairs (e−∣Pj∣,j)(e - |P_j|, j) with PjP_j a suffix of str(v)=λe\mathrm{str}(v) = \lambda_e (Lemmas 3 and 4), and λe\lambda_e is a suffix of T[0…e−1]T[0 \ldots e-1]. So Pj=T[e−∣Pj∣…e−1]P_j = T[e - |P_j| \ldots e - 1], and each output pair is an occurrence.

Conversely, let (s,j)(s, j) be an occurrence and e=s+∣Pj∣e = s + |P_j|, so 0≤e≤n0 \le e \le n. Then PjP_j is a suffix of T[0…e−1]T[0 \ldots e-1], and Pj∈NP_j \in N. By the definition of λe\lambda_e, ∣Pj∣≤∣λe∣|P_j| \le |\lambda_e|, so PjP_j is a suffix of λe\lambda_e. The search calls REPORT exactly once with the end position ee, at line L14 if e=0e = 0 and at line L18 with i=e−1i = e - 1 otherwise, and by Lemma 3 this call outputs (s,j)(s, j) exactly once. Other calls have another end position, so they do not output (s,j)(s, j). ■\blacksquare

Back to Aho-Corasick algorithm