Suffix Arrays, LCP & Aho–Corasick
A suffix array sorts all suffixes of a string, indexing every substring at once; built in , it locates a pattern by binary search in . Its companion LCP array (Kasai's algorithm) counts distinct substrings and finds the longest repeated substring.
╌╌╌╌
The string-matching lessons matched one pattern against a text, preprocessing the pattern. Two complementary needs remain. The first is to preprocess the text instead, so that afterwards any of many patterns can be queried fast — exactly the situation of a search engine or a genome browser, where the text is fixed and the patterns arrive online. The second is to match many patterns against one text in a single pass. This lesson builds the two canonical structures for these jobs: the suffix array (with its LCP array) for the first, and the Aho–Corasick automaton for the second. A short coda gives Manacher's algorithm, which finds every palindrome in linear time by the same Z-box bookkeeping we have already met.
Both halves keep the soundness / completeness discipline of a matcher: every reported occurrence must be genuine, and every genuine occurrence must be reported. What changes is where the preprocessing cost is paid and what a single index then enables.
Suffix arrays: sorting all the suffixes
Index the text once so that every substring question becomes cheap. A substring of is a prefix of some suffix of ; if we had all suffixes in sorted order, every occurrence of a pattern would form a contiguous block (all the suffixes that start with ), findable by binary search. That is the whole idea.
It is the array of starting positions, not the suffixes themselves — integers, where the suffixes laid out in full would be characters. This compactness is why the suffix array displaced the older suffix tree in practice: same indexing power, a fraction of the memory.1
It is convenient to append a sentinel $ that is smaller than every real
character and occurs nowhere else; it makes every suffix uniquely ordered (no
suffix is a prefix of another) and is shown above as [end].
Construction by prefix doubling
The naive build — sort the suffixes with a comparison sort — costs comparisons, but each comparison may scan characters, so overall. Prefix doubling (the Manber–Myers method) removes the per-compare blowup by sorting suffixes on growing prefixes of length , carrying a rank for each suffix between rounds.
The invariant is that after the round for length , every suffix carries a rank equal to its position among all suffixes ordered by their first characters (ties shared). To go from to , sort each suffix by the pair: the first component orders by the first characters, the second breaks ties using the next — which is exactly the rank, already computed, of the suffix starting further along.
bananak=1k=2i\mathrm{rank}_1[i]\mathrm{rank}_1[i+1](\mathrm{rank}_1[i],\,\mathrm{rank}_1[i+1])2\mathrm{rank}_2-113(1,3)24(3,1)$ (both spell na`), so each pair shares a rankRun to completion on banana$, the build takes two rounds beyond the
initial character ranks. Assigning first-character ranks
($ = 0, a = 1, b = 2, n = 3) gives
with the three a-suffixes (starts ) tied at rank and the two
n-suffixes (starts ) tied at rank . The round above sorts the pairs
and re-ranks, producing
Two ties survive: starts and still share rank (both suffixes begin
an) and starts and share rank (both begin na). The next round
compares two characters further out, i.e. the pairs
. Start gets the pair
while start gets
, so sorts first: this reproduces the
four-character comparison ana$ < anan, decided by the ranks of
the suffixes two positions along, with no characters rescanned. Likewise start
gets against start 's
, putting na$ before nana$. The new ranks
are all distinct, so the loop's early-exit test fires and reading the positions in rank order yields — the sorted column of the first figure.
The cost accounting: each round sorts pairs of integers drawn from , which two passes of counting sort (least-significant component first) do in time and space; re-ranking is one linear scan of the sorted order. The prefix length doubles each round, so rounds suffice, and the total is . Swapping the radix sort for a comparison sort costs per round instead, giving the easier-to-code variant in Algorithm 1. Specialised linear-time builds exist — the DC3 / skew algorithm and SA-IS — but doubling is the standard construction and the one to know.
- 1append sentinel;
- 2for allround k = 1: rank by single char
- 3
- 4
- 5while do
- 6define
- 7sort byradix sort for per round
- 8
- 9for to dore-rank, ties keep equal rank
- 10$\mathit{tmp}[\mathit{sa}[p]] \gets \mathit{tmp}[\mathit{sa}[p-1]]
- 11+ (key(\mathit{sa}[p]) \ne key(\mathit{sa}[p-1])\ ?\ 1 : 0)$
- 12
- 13if then breakall ranks distinct
- 14
- 15return
Pattern matching by binary search
Because the suffixes sit in sorted order, the suffixes that begin with occupy a contiguous range of . Two binary searches — for the lower and upper bounds of that range — locate every occurrence of .
- 1find first suffix
- 2while do
- 3
- 4if then else
- 5
- 6find first suffix all
- 7while do
- 8
- 9if has prefix then else
- 10report as the occurrence positions
an (rows and , starts and ) form one contiguous green block. A probe at the midpoint compares against that row's suffix and discards the half that cannot contain ; two such searches bracket the block's lower and upper endsThe comparisons each rescanning characters is the looseness here; storing the LCP array (next) lets a refined search shave it to . Soundness holds because a reported index lies in the range only if its suffix has as a prefix, i.e. genuinely occurs there; completeness holds because every occurrence is a suffix prefixed by , hence inside the contiguous range the two searches bracket.
The LCP array: what adjacency reveals
The suffix array alone does not record how much adjacent suffixes share. That shared length is what substring counting and comparison need.
For banana with the adjacent pairs share
: e.g. ana and anana (rows and ) share
the prefix ana of length .
ana, the longest repeated substringKasai's algorithm: LCP in linear time
A direct LCP computation compares adjacent suffixes character by character, . Kasai's algorithm does it in with one observation: process the suffixes in the order of the text, , and the LCP can drop by at most one each step, so it never has to be rescanned from scratch.
So we keep a running match length , start each suffix's comparison from rather than , and extend. The total of all increments is because rises by at most the number of matched characters and falls by at most one per step.
- 1compute as inverse ofrank[SA[r]] = r
- 2
- 3for to dosuffixes in text order
- 4if then
- 5predecessor in sorted order
- 6while and and do
- 7extend the shared prefix
- 8
- 9if thendrop at most one for next suffix
- 10else
- 11
- 12return
What the LCP array enables
Two classic results fall out immediately.
For banana, at the ana/anana pair, so ana is the
longest repeated substring — the basis of Longest Duplicate Substring (which
combines this with binary search on the answer, or a suffix array).
The same LCP array also lets a range-minimum query answer the longest-common-prefix of any two suffixes in after preprocessing — the basis of many competitive-programming string solutions.2
Aho–Corasick: matching a whole dictionary at once
Now the second job: find every occurrence of every pattern in a set within a text , in one pass. Running KMP times costs ; Aho–Corasick does it in where is the total number of matches reported, after preprocessing. It is, in one line, KMP generalised from a single string to a trie.
The construction has three layers.
1. The trie of patterns. Insert every pattern into a trie (one node per
distinct prefix of some pattern). A node is terminal if its root-path spells a
complete pattern. Following text characters down the goto edges of this trie is
exactly the naive idea of matching all patterns simultaneously — until a character
has no outgoing edge, at which point we are stuck.
2. Failure links. Just as KMP's jumps, on a mismatch, to the longest
proper prefix of the pattern that is also a suffix of what we matched,
Aho–Corasick's failure link points to the node whose
root-path is the longest proper suffix of 's root-path that is also a node
in the trie (a prefix of some pattern). When the text has no goto edge from the
current node, we follow failure links until an edge exists or we fall back to the
root.
3. Output links. A failure link can land on a terminal node, meaning a shorter pattern ends here too. Following the chain of failure links from any node and collecting terminals reports every pattern that ends at the current text position; precomputing these into output links makes that enumeration per reported match.
goto transitions (labelled by the character); each node shows the pref/ix it spells. Dashed blue edges are the four failure links that point somewhere other than the root (, , , ); every omitted failure link points to the root. Green double-ringed nodes are terminal (a pattern ends there)In the figure, the failure link from sh points to h (the longest suffix of
sh that is a trie node), and from she to he. The link from hers points to
s, and s's output chain is empty — but she's failure target he is terminal,
so scanning text …she… reports both she and the embedded he at the same
position. That overlap is precisely what output links capture.
ushers through the automaton. The active state walks down goto edges spelling she; at that terminal it reports she and (via its failure target, a terminal) the embedded he. Reading the next character r, state she has no goto on r, so the scan follows the blue failure link to he and continues he then her then hers, reporting hers at the end. Green marks the two reported matches; the dashed blue arrow is the failure jump- 1build trie of all patterns; mark terminal nodes
- 2
- 3empty queue
- 4for each child of dodepth-1 nodes fail to root
- 5enqueue
- 6while nonempty do
- 7dequeue
- 8for each labelled edge do
- 9enqueue
- 10
- 11while and has no edge on do
- 12climb failure links, like KMP
- 13( has edge on and that target ) ? target :
- 14plus ( terminal ? : none)
- 1
- 2for to do
- 3while and has no edge on do
- 4no goto: follow failure links
- 5if has edge on then that target
- 6for each pattern in do
- 7report ending at position
The structure is the same soundness-in-verification, completeness-in-coverage split we saw for single-pattern matching, now carried by the automaton's links rather than a single array. Aho–Corasick underlies dictionary scanners, intrusion-detection signature matching, and LeetCode's Stream of Characters (reverse the patterns and feed the stream backward into the automaton).3
Manacher's algorithm: all palindromes in
A short coda on a third linear-time string algorithm, included because it reuses the Z-box idea from the Z-function lesson almost verbatim. We want, for each centre, the radius of the longest palindrome centred there — which yields the longest palindromic substring and, summed, the count of all palindromic substrings.
To unify odd- and even-length palindromes, interleave a separator: transform into (with distinct end sentinels at both ends), so every palindrome of has odd length and a real centre. Then compute , the palindromic radius at each centre of .
The linear-time computation maintains the rightmost palindrome found so far, by centre and right edge (exactly the Z-box, now centred). For a new centre , its mirror gives a free lower bound ; then extend past by direct comparison and slide if the new palindrome reaches further right.
- 1interleave with separators and end sentinels
- 2
- 3for to do
- 4if then
- 5mirror bound
- 6while do
- 7extend past
- 8if then
- 9advance the box
- 10return
Each character is examined a constant number of times because only ever moves right, monotonically from to — the identical amortised argument as the Z-function. The longest palindromic substring is the centre of maximum radius (), and over real centres counts all palindromic substrings, both in .
Linear construction, bioinformatics, and self-indexes
Suffix arrays became practical when Manber and Myers (Suffix Arrays: A New Method for On-Line String Searches, SIAM J. Comput. 1993) introduced them explicitly as a space-frugal replacement for suffix trees, and the theory sharpened further when Kärkkäinen and Sanders gave the DC3 (skew) algorithm (Simple Linear Work Suffix Array Construction, ICALP 2003), which builds the suffix array in genuine time by a divide step that sorts two thirds of the suffixes recursively and merges in the rest. The prefix-doubling method in this lesson is the one most people implement, but the existence of linear-time construction is what lets suffix arrays index gigabyte-scale texts.
The reason suffix arrays matter far beyond exercises is bioinformatics and
compression. The Burrows–Wheeler transform (Burrows & Wheeler, A Block-
Sorting Lossless Data Compression Algorithm, 1994) is a permutation of the text
read directly off the sorted suffixes, and it is the basis of the bzip2
compressor; layered with the suffix array's rank information it becomes the FM-
index (Ferragina & Manzini, Opportunistic Data Structures with Applications,
FOCS 2000), a self-index that searches a compressed text without decompressing
it. Genome aligners such as bwa and bowtie are FM-indexes over a reference
genome — the same suffix-array machinery, made succinct.
Aho–Corasick, for its part, is the multi-pattern matcher (Aho & Corasick,
Efficient String Matching: An Aid to Bibliographic Search, CACM 1975) inside
classical tools like fgrep and inside intrusion-detection systems such as
Snort, which must scan every packet against thousands of signatures at once —
precisely the failure-automaton-on-a-trie construction this lesson builds.
Manacher's algorithm (Manacher, 1975) rounds out the trio as the linear-time
palindrome scanner, and all three share the amortised a pointer only moves right
argument that runs through this entire module.
Takeaways
- A suffix array lists the start indices of all suffixes in sorted order — integers indexing every substring. Prefix doubling builds it in (or with a comparison sort); pattern search is two binary searches in , since occurrences form a contiguous range.
- The LCP array records the shared prefix length of sorted-adjacent suffixes, computable in by Kasai's algorithm (the running match drops by at most one per text-order step). It gives the longest repeated substring () and the count of distinct substrings ().
- Aho–Corasick is KMP on a trie: a trie of all patterns plus failure links (longest proper-suffix node) and output links (chained terminals) scans the text once in , reporting every occurrence of every pattern. Soundness lives in the terminal output, completeness in the failure-link invariant; precompiling transitions yields a true DFA.
- Manacher's algorithm finds the palindromic radius at every centre in by the Z-box mirror-and-extend trick on a separator-interleaved string, giving the longest palindromic substring and the count of all palindromes.
Footnotes
- Skiena, § — Suffix Trees and Arrays: suffix arrays as the space-efficient successor to suffix trees, with binary-search substring queries. ↩
- Erickson, Ch. — String Matching: the LCP array, Kasai's linear-time construction, and the distinct-substring and longest-repeat corollaries. ↩
- CLRS, Ch. 32 — String Matching (§32.3–32.4): finite-automaton matching and the failure-function machinery that Aho–Corasick generalises from one pattern to a dictionary. ↩
╌╌ END ╌╌