String Algorithms: KMP, Z, Suffix Array

KMP, Z, Suffix Array

Read: ~50 min Advanced 6 practice problems Live code runner

1. The Pattern-Matching Problem

Given text T (length n) and pattern P (length m), find all occurrences of P in T. Naive: O(n·m). KMP, Z, and Rabin-Karp do it in O(n + m). Suffix arrays handle a wider family of substring queries in O(n log² n) or O(n log n) preprocessing.

Naive substring matching O(n·m) — যা ছোট ইনপুটে কাজ করে। বড় text-এ KMP / Z / rolling hash / suffix array — সব linear-time tool।

2. KMP Failure Function

For pattern P, compute fail[i] = length of the longest proper prefix of P[0..i] that is also a suffix. When matching against T, on a mismatch at P[j] we don't restart — we jump to fail[j-1]. Total comparisons O(n + m).

kmp.cpp
#include <bits/stdc++.h>
using namespace std;

vector<int> buildFail(const string& p) {
    int m = p.size();
    vector<int> fail(m, 0);
    int k = 0;
    for (int i = 1; i < m; i++) {
        while (k > 0 && p[k] != p[i]) k = fail[k - 1];
        if (p[k] == p[i]) k++;
        fail[i] = k;
    }
    return fail;
}

vector<int> kmpSearch(const string& t, const string& p) {
    vector<int> fail = buildFail(p), out;
    int k = 0, n = t.size(), m = p.size();
    for (int i = 0; i < n; i++) {
        while (k > 0 && p[k] != t[i]) k = fail[k - 1];
        if (p[k] == t[i]) k++;
        if (k == m) { out.push_back(i - m + 1); k = fail[k - 1]; }
    }
    return out;
}

int main() {
    for (int i : kmpSearch("ABABCABABABCABABABA", "ABABCAB"))
        cout << "match at " << i << "\n";
}

3. Z-Algorithm

For string S, z[i] = length of the longest substring starting at i that matches a prefix of S. Compute Z in O(n) using a sliding "Z-box". Pattern matching in T: build Z of P + '#' + T; positions where Z = |P| are matches.

Z-array KMP-এর alternative — অনেক সহজ implementation। কিছু ICPC contestant Z-array preferred করেন।

4. Rabin-Karp Rolling Hash

Compute a polynomial hash of P. Slide a window over T, updating the hash in O(1) per shift. On hash match, verify char-by-char. Expected O(n + m). Use two independent hashes (two primes) for collision safety.

Single hash is unsafe Adversarial inputs can force collisions. Always use double hashing (or modular bigint hashing) for any hash-based string algorithm in contests.

5. Suffix Array

Sort all n suffixes of S lexicographically; store their starting indices in sa[0..n−1]. Build in O(n log² n) with std::sort + comparator, or O(n log n) with the doubling algorithm. With the LCP array (longest common prefix between consecutive suffixes), suffix array answers many substring problems linearly.

suffix_array.cpp
#include <bits/stdc++.h>
using namespace std;

vector<int> buildSA(const string& s) {
    int n = s.size();
    vector<int> sa(n), rk(n), tmp(n);
    for (int i = 0; i < n; i++) { sa[i] = i; rk[i] = s[i]; }
    for (int k = 1; k < n; k *= 2) {
        auto cmp = [&](int a, int b) {
            if (rk[a] != rk[b]) return rk[a] < rk[b];
            int ra = a + k < n ? rk[a + k] : -1;
            int rb = b + k < n ? rk[b + k] : -1;
            return ra < rb;
        };
        sort(sa.begin(), sa.end(), cmp);
        tmp[sa[0]] = 0;
        for (int i = 1; i < n; i++)
            tmp[sa[i]] = tmp[sa[i - 1]] + (cmp(sa[i - 1], sa[i]) ? 1 : 0);
        rk = tmp;
        if (rk[sa[n - 1]] == n - 1) break;
    }
    return sa;
}

int main() {
    string s = "banana";
    for (int i : buildSA(s))
        cout << i << ": " << s.substr(i) << "\n";
}

6. Aho-Corasick (Preview)

For multi-pattern search (find all occurrences of any pattern from a set in a single text): build a trie of patterns, then add "failure links" similar to KMP. One pass of T finds every match across all patterns in O(|T| + matches). Used by spam filters, virus scanners, string-database lookups.

7. Practice Problems

  1. Longest happy prefix — longest proper prefix of S that is also a suffix.
    Longest happy prefix।
    ✨ Show Answer (উত্তর দেখুন)

    Approach: compute KMP failure function. Answer = S.substr(0, fail[n−1]).

  2. Shortest palindrome — prepend the fewest chars to make S a palindrome.
    Shortest palindrome।
    ✨ Show Answer (উত্তর দেখুন)

    Approach: let R = reverse(S). Compute KMP fail of S + '#' + R. fail[last] tells you the longest prefix of S that matches a suffix of R — that is the longest palindromic prefix. Prepend reverse of the rest.

  3. Repeated string match — given A, B, find min copies of A so that B is a substring.
    Repeated string match।
    ✨ Show Answer (উত্তর দেখুন)

    Approach: repeat A until length ≥ |B|; check if B is a substring (KMP). If not, append one more copy and check again. If still no, return −1.

  4. Longest substring repeated ≥ 2 times — solve via suffix array + LCP.
    Repeated substring — suffix array + LCP।
    ✨ Show Answer (উত্তর দেখুন)

    Approach: build SA + LCP. Answer = max(LCP[i]) — the longest LCP between consecutive sorted suffixes is the longest repeated substring.

  5. Count distinct substrings of S using suffix array.
    Distinct substring গুনা।
    ✨ Show Answer (উত্তর দেখুন)

    Approach: total substrings = n(n+1)/2. Subtract Σ LCP[i] (the number of duplicate prefixes). Answer = n(n+1)/2 − Σ LCP.

  6. Use Rabin-Karp double hashing to find a 5-character pattern in a 10⁵-character text.
    Double hashing দিয়ে pattern search।
    ✨ Show Answer (উত্তর দেখুন)

    Approach: two independent rolling hashes (mod 1e9+7 and 998244353). Match if both equal. Probability of collision ≈ 1/(10¹⁸).

Summary — Module 38

KMP and Z give linear-time pattern matching with failure functions. Rabin-Karp uses rolling hashes (always double-hash). Suffix arrays + LCP unlock most substring queries in O(n log n) preprocessing. Aho-Corasick handles multi-pattern in one pass. Together, this is the entire string-matching toolbox.

একটি pattern → KMP / Z। Multiple patterns → Aho-Corasick। যেকোনো substring query → suffix array।

Next Module → Computational Geometry & Number Theory Essentials।