Немного более простая для понимания альтернатива префикс-функции — z-функция.
Z-функция от строки определяется как массив , такой что равно длине максимальной подстроки, начинающейся с -й позиции, которая равна префиксу .
Наивно её реализовать ещё проще:
vector<int> slow_z_function(string s) {
int n = (int) s.size();
vector<int> z(n, 0); // z[0] считается не определенным
for (int i = 1; i < n; i++)
// если мы не вышли за границу и следующие символы совпадают
while (i + z[i] < n && s[z[i]] == s[i + z[i]])
z[i]++;
return z;
}
aaaaa
04321
abcdef
000000
abacabadaba
00103010301
Z-функцию можно использовать вместо префикс-функции в алгоритме Кнута-Морриса-Пратта — только теперь нужные позиции будут начинаться c , а не заканчиваться. Осталось только научиться её искать за .
#Как её быстро считать
Будем идти слева направо и хранить z-блок — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как и включительно.
Пусть мы сейчас хотим найти , а все предыдущие уже нашли. Новый -й символ может лежать либо правее z-блока, либо внутри него:
- Если правее, то мы просто наивно перебором найдем (максимальный отрезок, начинающийся с и равный префиксу), и объявим его новым z-блоком.
- Если -й элемент лежит внутри z-блока, то мы можем посмотреть на значение и использовать его, чтобы инициализировать чем-то, возможно, отличным от нуля. Если левее правой границы -блока, то — больше быть не может. Если он упирается в границу, то «обрежем» его до неё и будем увеличивать на единичку.
vector<int> z_function(string s) {
int n = (int) s.size();
vector<int> z(n, 0);
int l = 0, r = 0;
for (int i = 1; i < n; i++) {
// если мы уже видели этот символ
if (i <= r)
// то мы можем попробовать его инициализировать z[i - l],
// но не дальше правой границы: там мы уже ничего не знаем
z[i] = min(r - i + 1, z[i - l]);
// дальше каждое успешное увеличение z[i] сдвинет z-блок на единицу
while (i + z[i] < n && s[z[i]] == s[i + z[i]])
z[i]++;
// проверим, правее ли мы текущего z-блока
if (i + z[i] - 1 > r) {
r = i + z[i] - 1;
l = i;
}
}
return z;
}
Асимптотика. В алгоритме мы делаем столько же действий, сколько раз сдвигается правая граница z-блока — а это .
#Сравнение
В целом они зет- и префикс-функции очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про z-функцию почему-то мало кто знает кроме олимпиадных программистов.
Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение.
Упражнение 1. Дан массив префикс-функции. Исходная строка не дана. Вычислите за зет-функцию этой строки.
Упражнение 2. Дан массив зет-функции. Исходная строка не дана. Вычислите за префикс-функцию этой строки.