DSA DSA In Depth
0%
lesson lcp-arraykasaisuffix-array

Kasai's O(N) LCP Array Algorithm & Counting Distinct Substrings via SA + LCP

7 min read Module 4.2: Suffix Structures, Sequence Alignment & Data Compression Updated Oct 23, 2026
📄 Cheat Sheet
Kasai's LCP Array Algorithm and Suffix Array Applications
WilliamFiset Kasai's LCP Array Algorithm and Suffix Array Applications Watch on YouTube ↗

Demonstrates Kasai's h >= lcp - 1 invariant in text order and shows how SA + LCP immediately counts distinct substrings and finds longest repeated substrings.

0:00 / --:--

1. Kasai's Theorem: Why $\text{LCP}$ Drops by at Most $1$ in Text Order

Let $\text{rank}[i] \in [0, N-1]$ be the inverse permutation of $\text{SA}$, so $\text{SA}[\text{rank}[i]] = i$. Suppose suffix $i$ at rank $r = \text{rank}[i] < N - 1$ shares a longest common prefix of length $h > 0$ with the next suffix in sorted order, $j = \text{SA}[r + 1]$:

$$\text{LCP}(S[i..],\; S[j..]) = h > 0 \implies S[i..i+h-1] = S[j..j+h-1]$$

Now delete the first character $S[i] = S[j]$ from both suffixes! We obtain suffix $S[i+1..]$ and suffix $S[j+1..]$:

  1. Because $S[i..]$ is lexicographically smaller than $S[j..]$ and their first characters are identical ($h \ge 1$), suffix $S[i+1..]$ MUST still be lexicographically smaller than $S[j+1..]$, so $\text{rank}[i+1] < \text{rank}[j+1]$.
  2. Moreover, $S[i+1..]$ and $S[j+1..]$ share a common prefix of length exactly $h - 1$: $S[i+1..i+h-1] = S[j+1..j+h-1]$.
  3. By the LCP Range Minimum Property, any suffix lying lexicographically between $\text{rank}[i+1]$ and $\text{rank}[j+1]$ in $\text{SA}$ (specifically the immediate neighbor $\text{SA}[\text{rank}[i+1] + 1]$) must ALSO share a common prefix of length at least $h - 1$ with $S[i+1..]$!

Therefore, when moving from text index $i$ to $i + 1$, we simply set if (h > 0) --h; and start comparing characters from offset $h$ onward!

2. Three Classic Applications of $\text{SA} + \text{LCP}$

  • Arbitrary Suffix LCP via RMQ: For any two suffixes at ranks $r_1 < r_2$ in $\text{SA}$, the length of their longest common prefix is the minimum of adjacent LCPs between them: $$\text{LCP}(\text{SA}[r_1],\; \text{SA}[r_2]) = \min_{r_1 \le k < r_2} \text{lcp}[k]$$ Building a Sparse Table over $\text{lcp}[0..N-2]$ answers $\text{LCP}(i, j)$ for any two text positions in $O(1)$ time!
  • Counting Distinct Substrings in $O(N)$: Every substring of $S$ is a prefix of some suffix $\text{SA}[r]$. Suffix $\text{SA}[r]$ has length $N - \text{SA}[r]$, so it contributes $N - \text{SA}[r]$ prefixes in total, of which $\text{lcp}[r-1]$ were already counted as prefixes of the preceding suffix $\text{SA}[r-1]$. Summing the newly introduced prefixes across all ranks gives: $$\text{Distinct Substrings}(S) = \frac{N(N + 1)}{2} - \sum_{r=0}^{N-2} \text{lcp}[r]$$
  • Longest Repeated Substring: The maximum value $\max_{0 \le r < N-1} \text{lcp}[r]$ is the exact length of the longest substring that appears at least twice in $S$.
Problem / Query With Suffix Tree With Suffix Array + LCP (+ Sparse Table)
Build Structure + LCP $O(N \log |\Sigma|)$ ($40N$ bytes) $O(N \log N)$ or $O(N)$ ($8N$ bytes)
Number of Distinct Substrings Sum edge lengths $\sum (r_e - l_e)$ in $O(N)$ $\frac{N(N+1)}{2} - \sum \text{lcp}[r]$ in $O(N)$
Longest Common Prefix of $S[i..]$ and $S[j..]$ Lowest Common Ancestor (LCA) in $O(1)$ Range Minimum Query (RMQ) on $\text{lcp}$ in $O(1)$
Kasai Lower-Bound & LCP-RMQ Duality Invariant

For every text position $i \in [0, N-2]$ with $\text{rank}[i] < N - 1$, $\text{lcp}[\text{rank}[i + 1]] \ge \text{lcp}[\text{rank}[i]] - 1$. Furthermore, for any sorted ranks $0 \le l < r < N$, $\text{LCP}(\text{SA}[l], \text{SA}[r]) = \min_{l \le k < r} \text{lcp}[k]$, which is the exact array-based dual of the Lowest Common Ancestor (LCA) depth of leaves $l$ and $r$ in the Suffix Tree.

64-Bit Overflow in N(N + 1) / 2 Distinct Substring Counts

For a string of length $N = 10^5$, the total number of substrings $\frac{N(N + 1)}{2}$ is approximately $5 \times 10^9$, which overflows a signed 32-bit int (max $\approx 2.14 \times 10^9$). Always accumulate $\frac{N(N+1)}{2} - \sum \text{lcp}[r]$ in a 64-bit integer (int64_t / long long).

3. Visual Trace: Kasai LCP & Distinct Substrings on $S = \texttt{"banana"}$

Rank r SA[r] lcp[r-1] Suffix (Red = Shared w/ Prev, Green = New Distinct Prefixes) New Count 0 5 - a 1 - 0 = 1 1 3 1 ana 3 - 1 = 2 2 1 3 anana 5 - 3 = 2 3 0 0 banana 6 - 0 = 6 4 4 0 na 2 - 0 = 2 5 2 2 nana 4 - 2 = 2 Total Distinct Substrings = 6*7/2 - (1 + 3 + 0 + 0 + 2) = 21 - 6 = 15
flowchart LR A["Invert SA: rank[SA[r]] = r; init h = 0"] --> B["Loop text index i = 0 .. N-1"] B --> C{"rank[i] == N - 1?"} C -- "Yes (last suffix in SA)" --> D["h = 0; continue"] C -- "No" --> E["Let j = SA[rank[i] + 1]"] E --> F["While S[i + h] == S[j + h]: h++"] F --> G["lcp[rank[i]] = h; if (h > 0) h--"] G --> B D --> B
Suffix Array, Kasai O(N) LCP Array & Suffix Automaton (SAM)
SA + Kasai LCP O(N) · ≤ 2N−1 SAM States

Inspect the sorted Suffix Array and Kasai’s LCP[i] invariant (h ≥ LCP[rank[i]−1] − 1) on S = "banana$", or step through Suffix Automaton (SAM) endpos equivalence classes and state-splitting on "abcbc".

Step 1 / 10
Custom Input:
Timeline

Invariant & State

Kasai Invariant: LCP[rank[i]] ≥ LCP[rank[i−1]] − 1 (h decrements ≤ N times, increments ≤ N times -> O(N) total!)
🌳 Live 2D Hierarchical Tree Topology — Suffix Array, Kasai O(N) LCP Array & Suffix Automaton (SAM) Visited/Active Pivot/Root Resolved
LCP=0 SA=6 … [0] LCP=0 SA=5 … [1] LCP=1 SA=3 … [2] LCP=3 SA=1 … [3] LCP=0 SA=0 … [4] LCP=0 SA=4 … [5] LCP=2 SA=2 … [6]
[0]SA=6 "$"LCP=0
[1]SA=5 "a$"LCP=0
[2]SA=3 "ana$"LCP=1
[3]SA=1 "anana$"LCP=3
[4]SA=0 "banana$"LCP=0
[5]SA=4 "na$"LCP=0
[6]SA=2 "nana$"LCP=2
String S
"banana$" (N = 6)
∑ LCP[i]
0 + 1 + 3 + 0 + 0 + 2 = 6
Distinct Substrings
21 − 6 = 15
Step
1 / 4
[Step 1] Kasai at i=0 ("banana$"): rank=4, predecessor is "anana$" -> LCP = 0.

4. Production Implementation: Kasai's $O(N)$ LCP & Distinct Substring Counter

#include <cstdint>
#include <numeric>
#include <string_view>
#include <vector>

// Kasai's O(N) Algorithm: returns lcp[0..N-2] where lcp[r] = LCP(SA[r], SA[r+1])
std::vector<int> build_lcp_kasai(std::string_view s, const std::vector<int>& sa) {
    const int n = static_cast<int>(s.size());
    if (n <= 1) return {};

    std::vector<int> rank(n, 0);
    for (int r = 0; r < n; ++r) {
        rank[sa[r]] = r;
    }

    std::vector<int> lcp(n - 1, 0);
    int h = 0;
    for (int i = 0; i < n; ++i) {
        if (rank[i] == n - 1) {
            h = 0;
            continue;
        }
        int j = sa[rank[i] + 1];
        while (i + h < n && j + h < n && s[i + h] == s[j + h]) {
            ++h;
        }
        lcp[rank[i]] = h;
        if (h > 0) --h;
    }
    return lcp;
}

// Counts total distinct non-empty substrings of S in O(N) after SA + LCP construction
int64_t count_distinct_substrings(int64_t n, const std::vector<int>& lcp) {
    int64_t total_substrings = n * (n + 1) / 2;
    int64_t duplicate_prefixes = std::accumulate(lcp.begin(), lcp.end(), int64_t{0});
    return total_substrings - duplicate_prefixes;
}
Off-By-One Convention in LCP Indexing

Two conventions exist in textbooks and competitive programming libraries: some define $\text{lcp}[r] = \text{LCP}(\text{SA}[r], \text{SA}[r+1])$ of length $N - 1$ (so $\text{LCP}(\text{SA}[l], \text{SA}[r]) = \min_{l \le k < r} \text{lcp}[k]$), while others define $\text{lcp}[r] = \text{LCP}(\text{SA}[r-1], \text{SA}[r])$ of length $N$ with $\text{lcp}[0] = 0$ (so $\text{LCP}(\text{SA}[l], \text{SA}[r]) = \min_{l + 1 \le k \le r} \text{lcp}[k]$). Notice that in both conventions the RMQ interval spans $r - l$ elements, NOT $r - l + 1$! Querying $\min_{l \le k \le r} \text{lcp}[k]$ accidentally includes the boundary with $\text{SA}[r+1]$ (or $\text{SA}[l-1]$) and corrupts the LCP query.

Check yourself Question 1 of 4
Concept Check Kasai's Core Inequality recall

In Kasai's $O(N)$ LCP construction algorithm, we iterate through suffixes in text order $i = 0, 1, \dots, N-1$ rather than sorted Suffix Array order $r = 0, 1, \dots, N-1$.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

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

How is this all-pairs LCP sum reduced to a classic linear-time problem on Kasai's `lcp[0..N-2]` array?

Key Takeaways

Kasai's algorithm computes the LCP array in O(N) time by iterating in text order i = 0..N-1 and exploiting the inequality lcp[rank[i+1]] >= lcp[rank[i]] - 1.
The LCP of any two arbitrary suffixes at sorted ranks l < r equals the range minimum min_{l <= k < r} lcp[k], turning SuffixTree LCA queries into O(1) Sparse Table RMQ queries.
The number of distinct non-empty substrings in a string of length N is exactly N(N + 1)/2 - sum_{r=0}^{N-2} lcp[r].
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue