Определение. Префикс-функцией от строки называется массив , где равно длине самого большого префикса строки , который также является и суффиксом -того префика (не считая весь -й префикс).
Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна .
vector<int> slow_prefix_function(string s) {
int n = (int) s.size();
vector<int> p(n, 0);
for (int i = 1; i < n; i++)
for (int len = 1; len <= i; len++)
// если префикс длины len равен суффиксу длины len
if (s.substr(0, len) == s.substr(i - len + 1, len))
p[i] = len;
return p;
}
Этот алгоритм пока что работает за , но позже мы его ускорим.
#Как это поможет решить исходную задачу?
Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки, и научимся с помощью нее искать подстроку в строке.
Соединим подстроки и каким-нибудь символом, который не встречается ни там, ни там — обозначим пусть этот символ #
. Посмотрим на префикс-функцию получившейся строки s#t
.
string s = "choose";
string t =
"choose life. choose a job. choose a career. choose a family. choose a fu...";
cout << s + "#" + t << endl;
cout << slow_prefix_function(s + "#" + t) << endl;
choose#choose life. choose a job. choose a career. choose a family. choose a fu...
0000000123456000000012345600000000123456000100000001234560000000000012345600000000
Видно, что все места, где значения равны 6 (длине ) — это концы вхождений в текст .
Такой алгоритм (посчитать префикс-функцию от s#t
и посмотреть, в каких позициях она равна ) называется алгоритмом Кнута-Морриса-Пратта.
#Как её быстро считать
Рассмотрим ещё несколько примеров префикс-функций и попытаемся найти закономерности:
aaaaa
01234
abcdef
000000
abacabadava
00101230101
Можно заметить следующую особенность: максимум на единицу превосходит .
Доказательство. Если есть префикс, равный суффиксу строки , длины , то, отбросив последний символ, можно получить правильный суффикс для строки , длина которого будет ровно на единицу меньше.
Попытаемся решить задачу с помощью динамики: найдём формулу для через предыдущие значения.
Заметим, что в том и только том случае, когда . В этом случае мы можем просто обновить и пойти дальше.
Например, в строке выделен максимальный префикс, равный суффиксу: . Если следующий символ равен будет равен , то .
Но что происходит, когда ? Пусть следующий символ в этом же примере равен не , а .
- Длина префикса, равного суффиксу новой строки, будет точно меньше 5.
- Помимо того, что искомый новый супрефикс является суффиксом «aabaab», он ещё является префиксом подстроки «aabaa».
- Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть , которое мы уже посчитали.
- Если (т. е. новый символ совпадает с идущим после префикса-кандидата), то .
В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, ? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — . Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым.
vector<int> prefix_function(string s) {
int n = (int) s.size();
vector<int> p(n, 0);
for (int i = 1; i < n; i++) {
// префикс функция точно не больше этого значения + 1
int cur = p[i - 1];
// уменьшаем cur значение, пока новый символ не сматчится
while (s[i] != s[cur] && cur > 0)
cur = p[cur - 1];
// здесь либо s[i] == s[cur], либо cur == 0
if (s[i] == s[cur])
p[i] = cur + 1;
}
return p;
}
Асимптотика. В худшем случае этот while
может работать раз за одну итерацию, но в среднем каждый while
работает за .
Префикс-функция каждый шаг возрастает максимум на единицу и после каждой итерации while
уменьшается хотя бы на единицу. Значит, суммарно операций будет не более .