KMP
To bardzo efektywny algorytm szukania fragmentów tekstu. Używa przedprocesowania fragmentu, aby mądrze obsługiwać niezgodności, przez co osiąga złożoność liniową.
Idea
Jak można szukać tekstu w tekście? Wbrew pozorom jest to istotna funkcja, potrzebna w wielu dziedzinach informatyki (i nie tylko).
W dalszych podpunktach będziemy odnosić się do fragmentu szukanego jako
Algorytm naiwny
Możemy iść po
KMP
Zaczniemy od stworzenia tablicy
Na przykład dla fragmentu aabcdcdaab, najdłuższym takim jest aab.
Teraz jeżeli weźmiemy pierwsze
Przykładowa konstrukcja lps
Wzorzec: "aabaaac"
- Indeks 0: "a" → Brak właściwego prefiksu/sufiksu →
lps[0] = 0 - Indeks 1: "aa" → "a" jest zarówno prefiksem, jak i sufiksem →
lps[1] = 1 - Indeks 2: "aab" → Żaden prefiks nie pasuje do sufiksu →
lps[2] = 0 - Indeks 3: "aaba" → "a" jest prefiksem i sufiksem →
lps[3] = 1 - Indeks 4: "aabaa" → "aa" jest prefiksem i sufiksem →
lps[4] = 2 - Indeks 5: "aabaaa" → "aa" jest prefiksem i sufiksem (tutaj najdłuższym dopasowaniem jest "aa") →
lps[5] = 2 - Indeks 6: "aabaaac" → Brak dopasowania (mismatch), następuje reset →
lps[6] = 0
Jak sprawnie obliczyć lps
Ponieważ tablica 1-elementowa nie ma poprawnych prefixów, które są suffixami, wpisujemy
Idziemy przez
Przypadek 1:
To oznacza, że nowy znak kontynuuje istniejące dopasowanie.
- Zwiększamy
o 1 - idziemy do kolejnego indeksu
Przypadek 2:
Nie ma żadnego pasującego prefixu, więc nie możemy wrócić do poprzedniego spasowania.
- idziemy do kolejnego indeksu
Przypadek 3:
Nie możemy kontynuować istniejącego ciągu, jednak może istnieć krótszy ciąg, który zadziała. Ponieważ
- nie zmieniamy i
Kod
vector<int> computeLPSArray(string &pattern) {
int n = pattern.size();
vector<int> lps(n, 0);
int len = 0;
int i = 1;
while (i < n) {
if (pattern[i] == pattern[len]) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
// wracamy do starego wzoru
len = lps[len - 1];
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
Dlaczego LPS jest takie ważne?
Tablica LPS (Longest Prefix Suffix) pozwala nam uniknąć bezcelowego sprawdzania tych samych znaków po raz drugi. W algorytmie naiwnym, po wykryciu niezgodności, cofamy się do początku wzorca. W KMP, dzięki LPS, wiemy dokładnie, o ile pozycji możemy bezpiecznie przesunąć nasz wzorzec, nie tracąc przy tym szansy na znalezienie dopasowania.
Wykorzystujemy fakt, że jeśli doszło do niezgodności na pozycji
Działanie Algorytmu KMP
Mając przygotowaną tablicę LPS, możemy przystąpić do przeszukiwania tekstu
Przebieg wyszukiwania:
- Używamy dwóch wskaźników:
dla tekstu oraz dla wzorca . - Jeśli znaki
oraz są zgodne, zwiększamy oba wskaźniki. - Jeśli
osiągnie długość wzorca, oznacza to znalezienie dopasowania. Wtedy przeskakuje do wartości , aby szukać kolejnych wystąpień. - W przypadku niezgodności (
): - Jeśli
, zmieniamy na (nie cofamy wskaźnika !). - Jeśli
, po prostu zwiększamy , przechodząc do kolejnego znaku w tekście
- Jeśli
Złożoność czasowa wyszukiwania wynosi
vector<int> computeLPSArray(string &pattern) {
int n = pattern.size();
vector<int> lps(n, 0);
int len = 0;
int i = 1;
while (i < n) {
if (pattern[i] == pattern[len]) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
// wracamy do starego wzoru
len = lps[len - 1];
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
void KMPSearch(string text, string pattern) {
int n = text.size();
int m = pattern.size();
// Przygotowanie tablicy pomocniczej
vector<int> lps = computeLPSArray(pattern);
int i = 0; // wskaźnik dla tekstu (text)
int j = 0; // wskaźnik dla wzorca (pattern)
while (i < n) {
if (pattern[j] == text[i]) {
i++;
j++;
}
if (j == m) {
cout << "Wzorzec znaleziony na indeksie: " << i - j << endl;
// Przygotowanie do szukania kolejnych wystąpień
j = lps[j - 1];
}
else if (i < n && pattern[j] != text[i]) {
if (j != 0) {
// Dzięki LPS nie cofamy się w tekście (i zostaje w miejscu)
j = lps[j - 1];
} else {
// Jeśli j jest na początku, po prostu przesuwamy się w tekście
i++;
}
}
}
}