Algoritmo KMP: Função de Prefixo e Busca de Padrões em C++

Nota: todos os índices utilizados neste artigo são baseados em zero.

Prefixos e sufixos

Dado uma string s, um prefixo é qualquer substring que inicia na primeira posição de s. O prefixo que termina na posição i é denotado por s[0..i]. Um prefixo próprio é todo prefixo de s diferente dela mesma.

De forma análoga, um sufixo é uma substring que termina na última posição de s. Um sufixo próprio é qualquer sufixo de s exceto a própria string.

Por exemplo, para s = "abcabeda", seus prefixos próprios são "a", "ab", "abc", "abca", "abcabe", "abcabed", enquanto seus sufixos próprios incluem "a", "da", "eda", "beda", "abeda", "cabeda", "bcabeda".

Função de prefixo (função pi)

A função de prefixo de uma string s, geralmente denotada por pi, associa a cada posição i o comprimento do maior prefixo próprio de s[0..i] que também é sufixo dessa substring.

Formalmente:

pi[i] = max { k | 0 ≤ k ≤ i e s[0..k-1] = s[i-k+1..i] }

Por definição, pi[0] = 0, pois uma substring de um único caractere não possui prefixo próprio.

Exemplo

Para s = "abacaba":

  • pi[0] = 0 — apenas "a";
  • pi[1] = 0 — "ab" não tem prefixo e sufixo comuns;
  • pi[2] = 1 — em "aba", "a" é prefixo e sufixo próprio;
  • pi[3] = 0 — "abac" não possui coincidência;
  • pi[4] = 1 — em "abaca", apenas "a" coincide;
  • pi[5] = 2 — em "abacab", "ab" é prefixo e sufixo;
  • pi[6] = 3 — em "abacaba", "aba" é o maior prefixo/sufixo comum.

Construção eficiente da função de prefixo

Uma observação importante é que, ao avançarmos para a próxima posição, o valor de pi[i] pode aumentar no máximo uma unidade. Se o próximo caractere continua o prefixo já encontrado, incrementamos o valor; caso contrário, recuamos para o maior prefixo próprio anterior e tentamos novamente.

Essa ideia permite calcular toda a função em tempo linear.

void prefix_function(const string &s) {
    int n = s.size();
    pi[0] = 0;
    for (int i = 1; i < n; ++i) {
        int j = pi[i - 1];
        while (j > 0 && s[i] != s[j]) {
            j = pi[j - 1];
        }
        if (s[i] == s[j]) {
            ++j;
        }
        pi[i] = j;
    }
}

Algoritmo de Knuth-Morris-Pratt

O KMP resolve o problema de encontrar todas as ocorrências de um padrão p dentro de um texto t. A ideia central é calcular a função de prefixo para a string concatenada p + '#' + t, onde '#' é um caractere separador que não aparece em p nem em t.

Ao processarmos essa concatenação, quando pi[i] for igual ao comprimento de p, isso significa que o padrão ocorre no texto terminando na posição correspondente a i. Convertendo o índice para o texto original, a posição inicial (0-indexada) da ocorrência é i - 2 * |p|.

Implementação completa

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

const int MAXN = 1e6 + 5;
int pi[MAXN];

void prefix_function(const string &s) {
    int n = s.size();
    pi[0] = 0;
    for (int i = 1; i < n; ++i) {
        int j = pi[i - 1];
        while (j > 0 && s[i] != s[j]) {
            j = pi[j - 1];
        }
        if (s[i] == s[j]) {
            ++j;
        }
        pi[i] = j;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string padrao, texto;
    cin >> padrao >> texto;

    string concat = padrao + "#" + texto;
    prefix_function(concat);

    int p = padrao.size();
    for (int i = p + 1; i < (int)concat.size(); ++i) {
        if (pi[i] == p) {
            cout << (i - 2 * p) << '\n';
        }
    }

    return 0;
}

Assintoticamente, o algoritmo consome tempo O(|p| + |t|) e memória O(|p| + |t|) para armazenar a concatenação e os valores de pi.

Tags: KMP algoritmo-de-Knuth-Morris-Pratt função-de-prefixo Busca-de-Padroes Strings

Publicado em 9-15 00:24