Worked examples — Suffix array — construction O(n log n), LCP array
This is the hands-on companion to the parent Suffix-Array note. The parent built the machinery; here we run that machinery across every kind of input a problem can hand you — including the weird degenerate ones that break naive code.
The scenario matrix
Every string you will ever feed a suffix array falls into one of these cells. The final column names the example that pins it down.
| Cell | Scenario class | What could break | Example |
|---|---|---|---|
| A | Generic mixed string | prefix-doubling rounds, sentinel | Ex 1 (mississippi) |
| B | All characters equal (aaaa) |
every suffix shares a prefix; LCP is a staircase | Ex 2 |
| C | All characters distinct (abcd) |
first round already finishes; LCP all zero | Ex 3 |
| D | Length 1 (a) |
one suffix, no adjacent pair, no LCP entry | Ex 4 |
| E | Empty string () | loops must not run; array is empty | Ex 4 |
| F | Kasai "drop by at most 1" reuse | resetting wrongly → | Ex 5 |
| G | Word problem — pattern search | binary-search block must be contiguous | Ex 6 |
| H | Counting distinct substrings via LCP | off-by-one in the two sums | Ex 7 |
| I | Exam twist — two suffixes' LCP via RMQ | LCP of non-adjacent pair needs a range-min | Ex 8 |
Cells A–I are all covered below. Let us walk them.
Ex 1 — Cell A: a generic string, full doubling
Ex 2 — Cell B: all characters equal
Ex 3 — Cell C: all characters distinct
Ex 4 — Cells D & E: length 1 and empty (degenerate)
Ex 5 — Cell F: watching Kasai's "drop by at most 1" reuse
, , and watch Forecast: Between two consecutive -steps, how much can fall? Guess the maximum.
First we need . Suffixes: 0 aab, 1 ab, 2 b. Sorted: aab < ab < b.
Step 1 — , rank : no predecessor.
Why? Suffix aab is first in sorted order. Set , . Then .
Step 2 — , rank : predecessor .
Why? Compare with . Match a (pos 0), then b vs a mismatch → . Set . Then .
Step 3 — , rank : predecessor .
Why? Compare with . First char b vs a mismatch → . Set .
The reuse in action: the drop from to the carried before Step 3 was exactly 1 — never more. That guaranteed cap is why the total work is : each character index can only be "re-scanned" a bounded number of times.
each loop It would still give the right LCP here (small string), but on strings with long shared prefixes it re-scans from scratch every time → . Carry over.
Verify: , and distinct substrings : a, aa, aab, ab, b. ✅
Ex 6 — Cell G: word problem, pattern search
"an" occur in "banana"?
Forecast: All matches sit in one contiguous block of the suffix array. Guess how big that block is.
From the parent: for banana, i.e. sorted suffixes
r=0 SA=5 a
r=1 SA=3 ana
r=2 SA=1 anana
r=3 SA=0 banana
r=4 SA=4 na
r=5 SA=2 nana
Step 1 — a match = a suffix whose prefix is "an".
Why this step? Every occurrence of pattern in is a suffix that starts with . Because suffixes are sorted, those form one contiguous run — binary-searchable.
Step 2 — binary search the lower and upper bound of prefix "an".
Why this step? We want the first sorted suffix "an" and the first "an\xff" (anything just past the "an…" block).
- Lower bound:
"ana"at (it is"an"). - Upper bound:
"banana"at is the first not starting with"an". - So the block is .
Step 3 — read off the occurrences.
Block size . Starting positions and .
banana → b a n a n a; positions 1 (anana) and 3 (banana). 2 occurrences.
Verify: scanning banana by hand, an appears at index 1 and index 3. Count . ✅
Ex 7 — Cell H: counting distinct substrings (formula drill)
,
Forecast: abab clearly repeats a lot. Will distinct count be closer to 10 (all) or to 6?
Step 1 — build . Suffixes: 0 abab, 1 bab, 2 ab, 3 b. Sorted: ab < abab < b < bab.
Step 2 — build LCP.
- :
abvsababshareab→ . - :
ababvsbshare nothing → . - :
bvsbabshareb→ .
Step 3 — apply the counting formula. Why this formula? counts all substrings (each suffix contributes its length-many prefixes). removes the ones counted twice because adjacent suffixes shared a prefix.
Verify: list them — a, ab, aba, abab, b, ba, bab = 7. ✅ (Closer to 6 than 10 — repetition costs distinctness.)
Ex 8 — Cell I: exam twist, LCP of two non-adjacent suffixes
and in "banana"
Forecast: They are NOT neighbors in . So the LCP array alone doesn't give the answer directly. Guess the value, then see the trick.
Recall , , .
Step 1 — locate both suffixes in sorted order. Why this step? The LCP of any two suffixes equals the minimum LCP over the sorted-order stretch between them. So first find their ranks.
- (suffix
banana). - (suffix
nana).
Step 2 — take the range minimum of LCP. Why this step? Walking from rank 3 to rank 5 in sorted order, each adjacent pair shares only as much as its LCP; the shared prefix of the endpoints can be no longer than the smallest LCP along the way. This is a range-minimum query → use a Sparse Table. (We take LCP over indices of the larger, i.e. and .)
Step 3 — interpret.
LCP : banana (starts b) and nana (starts n) share no prefix.
Verify: banana vs nana — first chars b ≠ n. Common prefix length . ✅
Recall Why RMQ and not a fresh scan?
A fresh character-by-character compare is per query. With the LCP array plus RMQ precomputed in , each "LCP of two suffixes" query drops to . That is the whole payoff of the LCP array.
Recap
Recall Which cell did each example cover?
Ex 1 ::: A — generic string, full prefix-doubling Ex 2 ::: B — all equal characters (staircase LCP) Ex 3 ::: C — all distinct (one round, zero LCP) Ex 4 ::: D and E — length 1 and empty string Ex 5 ::: F — Kasai's carry-over of , drop-by-1 Ex 6 ::: G — pattern search via contiguous block Ex 7 ::: H — distinct-substring counting formula Ex 8 ::: I — LCP of non-adjacent suffixes via RMQ
"E-D-B-C-F" — Empty, length-one (D), all-equal (B, longest→last), all-distinct (C, one round), and Kasai's carried F (never reset ).
Related tools worth revisiting: Radix Sort and Counting Sort (fast per-round sorting), Suffix Tree (the heavier cousin), Z-Algorithm and KMP (single-pattern alternatives), and Burrows–Wheeler Transform (built directly from the sorted-suffix order).