Что такое префикс?
Префикс строки — это начало строки.
Например, строка:
abcde
Ее префиксы:
a
ab
abc
abcd
abcde
Что такое суффикс?
Суффикс строки — это конец строки.
Например, строка:
abcde
Ее суффиксы:
e
de
cde
bcde
abcde
Что такое префикс-функция?
Префикс-функция для строки s — это массив pi.
pi[i] — это длина наибольшего собственного префикса строки s[0...i], который одновременно является суффиксом этой же строки.
ищем самый длинный префикс
который равен суффиксу
но не равен всей строке
Простой пример
Строка:
ababa
Рассмотрим всю строку ababa.
Ее префиксы:
a
ab
aba
abab
ababa
Ее суффиксы:
a
ba
aba
baba
ababa
Самый длинный собственный префикс, который равен суффиксу:
aba
Значит для последней позиции:
pi[4] = 3
Пример массива pi
Для строки:
ababa
Префикс-функция:
s: a b a b a
i: 0 1 2 3 4
pi: 0 0 1 2 3
Объяснение:
- pi[0] = 0, потому что у одной буквы нет собственного непустого префикса
- pi[1] = 0, строка ab, нет совпадения
- pi[2] = 1, строка aba, совпадает a
- pi[3] = 2, строка abab, совпадает ab
- pi[4] = 3, строка ababa, совпадает aba
Еще пример
Строка:
aaaaa
Префикс-функция:
s: a a a a a
i: 0 1 2 3 4
pi: 0 1 2 3 4
Почему так? Потому что каждая следующая буква продолжает совпадение.
Идея алгоритма
Хотим посчитать pi[i]. Пусть для предыдущей позиции уже известно значение:
j = pi[i - 1]
Это значит, что у строки s[0...i-1] уже есть совпавший префикс длины j.
Теперь хотим добавить символ s[i].
если s[i] != s[j], надо откатиться на меньший префикс
Что значит откат?
Если символы не совпали, мы не начинаем проверку с нуля. Мы используем уже посчитанные значения pi.
Было:
j = pi[i - 1]
Если s[i] != s[j], то делаем:
j = pi[j - 1]
Это значит: переходим к следующей меньшей границе.
Основной код префикс-функции
vector<int> prefix_function(string s) {
int n = s.size();
vector<int> pi(n);
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;
}
return pi;
}
Полная программа
#include <bits/stdc++.h>
using namespace std;
vector<int> prefix_function(string s) {
int n = s.size();
vector<int> pi(n);
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;
}
return pi;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
cin >> s;
vector<int> pi = prefix_function(s);
for (int x : pi) {
cout << x << ' ';
}
return 0;
}
Input / Output
Input
ababa
Output
0 0 1 2 3
Разбор работы алгоритма
Пусть:
s = ababa
Идем по строке:
i = 1, s[i] = b
j = pi[0] = 0
s[1] != s[0]
pi[1] = 0
i = 2, s[i] = a
j = pi[1] = 0
s[2] == s[0]
pi[2] = 1
i = 3, s[i] = b
j = pi[2] = 1
s[3] == s[1]
pi[3] = 2
i = 4, s[i] = a
j = pi[3] = 2
s[4] == s[2]
pi[4] = 3
Сложный пример с откатом
Строка:
abacaba
Префикс-функция:
s: a b a c a b a
i: 0 1 2 3 4 5 6
pi: 0 0 1 0 1 2 3
На позиции i = 3 символ c не продолжает префикс aba, поэтому алгоритм делает откат.
Почему алгоритм O(n)?
Внутри есть while, но суммарно он не делает O(n²).
Переменная j может увеличиваться только на 1 за шаг, а при откате уменьшается по значениям pi.
и не может уменьшиться больше n раз
поэтому суммарно O(n)
Зачем нужна префикс-функция?
Префикс-функция используется в строковых алгоритмах.
2. алгоритм KMP
3. поиск всех вхождений pattern в text
4. поиск периода строки
5. работа с границами строки
Поиск подстроки через KMP
Нужно найти все вхождения строки pattern в строку text.
Создаем новую строку:
pattern + "#" + text
Символ # должен быть таким, которого нет в строках.
Если где-то значение pi равно длине pattern, значит pattern закончился в этой позиции.
Код KMP
#include <bits/stdc++.h>
using namespace std;
vector<int> prefix_function(string s) {
int n = s.size();
vector<int> pi(n);
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;
}
return pi;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string pattern, text;
cin >> pattern >> text;
string s = pattern + "#" + text;
vector<int> pi = prefix_function(s);
int m = pattern.size();
for (int i = 0; i < (int)s.size(); i++) {
if (pi[i] == m) {
int pos = i - 2 * m;
cout << pos << ' ';
}
}
return 0;
}
Пример KMP
Input
aba
abacaba
Output
0 4
Потому что aba встречается в строке abacaba на позициях:
0 и 4
Почему позиция i - 2 * m?
Мы строим строку:
pattern + "#" + text
Длина pattern равна m. Перед text стоит:
m символов pattern + 1 символ #
Если на позиции i в общей строке найдено совпадение длины m, то начало в text:
i - 2 * m
Граница строки
Граница строки — это строка, которая одновременно является префиксом и суффиксом, но не равна всей строке.
Например:
s = ababa
Границы:
a
aba
Самая длинная граница имеет длину:
pi[n - 1]
Найти все границы строки
Можно начать с:
k = pi[n - 1]
Потом переходить:
k = pi[k - 1]
Так мы найдем все длины границ.
Код: все границы строки
#include <bits/stdc++.h>
using namespace std;
vector<int> prefix_function(string s) {
int n = s.size();
vector<int> pi(n);
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;
}
return pi;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
cin >> s;
vector<int> pi = prefix_function(s);
vector<int> borders;
int k = pi[(int)s.size() - 1];
while (k > 0) {
borders.push_back(k);
k = pi[k - 1];
}
reverse(borders.begin(), borders.end());
for (int x : borders) {
cout << x << ' ';
}
return 0;
}
Пример границ
Input
ababa
Output
1 3
Это длины границ:
a
aba
Период строки
Период строки — это такая длина блока, который повторяется и образует всю строку.
Например:
ababab
Имеет период:
2
Потому что:
ab ab ab
Минимальный период через prefix function
Пусть:
n = s.size()
k = pi[n - 1]
Тогда кандидат на минимальный период:
period = n - k
Если n % period == 0, значит это настоящий период. Иначе минимальный период равен n.
Код: минимальный период строки
#include <bits/stdc++.h>
using namespace std;
vector<int> prefix_function(string s) {
int n = s.size();
vector<int> pi(n);
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;
}
return pi;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
cin >> s;
int n = s.size();
vector<int> pi = prefix_function(s);
int period = n - pi[n - 1];
if (n % period == 0) {
cout << period;
} else {
cout << n;
}
return 0;
}
Пример периода
Input
ababab
Output
2
Строка состоит из повторов:
ab ab ab
Подсчет вхождений каждого префикса
Иногда нужно узнать, сколько раз каждый префикс строки встречается внутри строки.
Сначала считаем prefix function. Потом каждое значение pi[i] увеличивает счетчик соответствующего префикса.
Код: количество вхождений префиксов
vector<int> count_prefixes(string s) {
int n = s.size();
vector<int> pi = prefix_function(s);
vector<int> cnt(n + 1);
for (int i = 0; i < n; i++) {
cnt[pi[i]]++;
}
for (int i = n; i > 0; i--) {
cnt[pi[i - 1]] += cnt[i];
}
for (int i = 0; i <= n; i++) {
cnt[i]++;
}
return cnt;
}
Асимптотика
| Задача | Время | Память |
|---|---|---|
| Префикс-функция | O(n) | O(n) |
| Поиск pattern в text | O(n + m) | O(n + m) |
| Все границы строки | O(n) | O(n) |
| Минимальный период | O(n) | O(n) |
Когда использовать prefix function?
| Задача | Что использовать |
|---|---|
| Найти pattern в text | KMP |
| Найти все границы строки | pi[n - 1], потом откаты |
| Найти минимальный период | n - pi[n - 1] |
| Работа с совпадениями префикса и суффикса | prefix function |
Типичные ошибки
- Путать префикс и суффикс
- Думать, что pi[i] считается для всей строки, а не для s[0...i]
- Забыть, что pi[0] всегда 0
- В while писать if и делать только один откат
- Забыть условие j > 0 в while
- В KMP выбрать разделитель, который встречается в строках
- Ошибиться с индексом позиции в KMP
- Путать 0-indexed и 1-indexed позиции
Короткий шаблон
vector<int> prefix_function(string s) {
int n = s.size();
vector<int> pi(n);
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;
}
return pi;
}
Главное запомнить
который равен суффиксу строки s[0...i]
если символы совпали → j++
если не совпали → j = pi[j - 1]
prefix function работает за O(n)
KMP ищет pattern в text за O(n + m)