Z-функция строки за O(n): построение и применение

Z-функция - компактная характеристика строки, на которой строится едва ли не половина олимпиадного арсенала по работе с текстами: поиск подстроки, подсчёт различных подстрок, определение периодов. Идея простая: для каждой позиции хранится длина наибольшего префикса исходной строки, который начинается именно отсюда. Полный массив строится за с одним указателем «правого края уже изученной области».
Определение через максимальный префикс
Пусть - строка длины , индексация с нуля. Z-функция - это массив той же длины, где
Словами: - длина наибольшего префикса , совпадающего с подстрокой, начинающейся в позиции . По соглашению либо не определяется, либо полагается равным (вся строка - префикс самой себя) - на алгоритм это никак не влияет, главное - не использовать в применениях.
Простой пример: для
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | |
|---|---|---|---|---|---|---|---|
| a | a | b | c | a | a | b | |
| - | 1 | 0 | 0 | 3 | 1 | 0 |
, потому что с позиции 4 идёт - это первые 3 символа . : совпадает только , дальше второй символ - это , а . Всё остальное - нули, потому что соответствующие символы не равны .
Отличие от префикс-функции
Z-функция и префикс-функция из алгоритма Кнута-Морриса-Пратта родственны, но смотрят на строку с разных концов. - длина наибольшего собственного префикса подстроки , который одновременно является её суффиксом: фокус на отрезке от 0 до , и совпадение проверяется в конце. - фокус на позиции и проверяется, как далеко вперёд тянется совпадение с префиксом всей строки.
Формально обе функции взаимовыразимы за : по можно восстановить и наоборот. На практике Z-функция чаще проще в реализации (меньше тонких case'ов с while-циклом по ), а удобнее, когда нужны именно «откаты» в KMP-стиле. Многие задачи решаются обеими - выбор сводится к привычке.
Наивный и идея ускорения
Наивный алгоритм - посимвольно сравнивать и , пока совпадает. Худший случай - строка из одинаковых символов : каждое , суммарно операций.
Ускорение опирается на наблюдение: если мы уже посчитали для и знаем самый правый «Z-блок» , где - конец совпавшего префикса, - то значения внутри блока можно частично восстановить без сравнений символов. Подстрока совпадает с , и любую позицию внутри можно сопоставить с зеркальной в начале строки и использовать как нижнюю оценку.
Алгоритм по шагам с двумя случаями
Поддерживаем пару - самый правый Z-блок, изначально . Для каждого от 1 до :
Случай 1: . Z-блок нас не покрывает, ничего полезного из прошлого не вытащить - стартуем «голое» сравнение и с нуля и увеличиваем , пока символы совпадают. После - обновляем , , если .
Случай 2: . Внутри блока. Зеркало , и мы знаем . Возможны две ветки:
- - зеркальное значение меньше, чем расстояние до правой границы. Тогда железно: дальше тянуть нельзя, потому что в зеркальной позиции совпадение уже оборвалось, а у нас та же подстрока.
- - зеркало упирается в границу, и за неё мы не знаем, что там. Берём нижнюю оценку и пытаемся «дотянуть» сравнением символов начиная с позиции . Если получилось - обновляем (и ).
Псевдокод:
l = r = 0
for i in 1..n-1:
if i <= r:
z[i] = min(r - i + 1, z[i - l])
while i + z[i] < n and s[z[i]] == s[i + z[i]]:
z[i] += 1
if i + z[i] - 1 > r:
l = i
r = i + z[i] - 1
Шесть строк - весь алгоритм. Никаких массивов алфавита, никаких отдельных билдеров.
Сложность через амортизированный анализ
Внешний цикл - итераций. Сравнения символов делаются только в while, и каждое успешное сравнение увеличивает как минимум на единицу. Указатель монотонно растёт и не превосходит - всего успешных сравнений не больше . Неуспешных - тоже не больше : по одному на итерацию внешнего цикла. Итого все сравнения суммарно , плюс административных операций - времени и памяти под массив. Анализ почти дословно повторяет KMP: «работа равна суммарному движению одного указателя».
Применения
**Поиск подстроки в через P\Ts = P + $ + T$PTzsiTz[i] = |P|O(|P| + |T|)$ - функционально эквивалентно KMP, реализация короче.
Число различных подстрок. Добавляя по одному символу в конец строки длины , считаем Z-функцию для развёрнутой длины - новых подстрок добавляется . Просуммировав, получаем ответ за . Не самый быстрый способ (суффиксный массив даёт ), но самый простой.
Периоды строки. - период , если для всех . Эквивалентно: . Перебрав все , за линейное время находим все периоды.
Палиндромы через комбинацию с Манакером. Z-функция для даёт палиндромные префиксы. Сам алгоритм Манакера работает быстрее, но в задачах с дополнительными ограничениями такой гибрид иногда удобнее.
Сравнение с KMP
Z-функция и префикс-функция - две стороны одной медали. Из строится за и наоборот. Обе работают за линию, обе используют амортизированный анализ через один монотонный указатель.
Различия - в форме изложения. описывает «что начинается в позиции »; - «что заканчивается в ». Олимпиадные шаблоны чаще берут Z - за десяток строк кода и однозначную интерпретацию. KMP остаётся стандартом в учебниках и в реализациях String.indexOf.
Типовые задачи (Codeforces)
- CF 126B Password. Самый длинный префикс, который встречается ещё как минимум один раз и в середине, и в конце. Префикс длины - суффикс, если ; встречается в середине - если .
- CF 432D Prefixes and Suffixes. Все префиксы строки, которые одновременно являются суффиксами, и число вхождений каждого. Z-функция даёт это в одну итерацию.
- CF 119D String Transformation. Минимальная циклическая ротация - через Z или Booth, обе линейные.
Частые ошибки
- Используют как полноценное значение. по соглашению, но в большинстве задач его явно исключают: «префикс совпадает сам с собой» - тривиальный факт, не несущий информации.
- **Забывают про разделитель \$$ при поиске PTPTz[i] > |P|$ - потом непонятно, что считать вхождением.
- Сравнивают с вместо . Off-by-one: блок включает обе границы, расстояние до конца - , а не .
- Пытаются обновить при . Если ни одного символа не совпало, обновлять блок нельзя - иначе может уменьшиться.
- Считают, что Z-функция полностью эквивалентна KMP по сложности и поэтому взаимозаменяема в любой задаче. Для одних задач удобнее , для других ; в задачах с DP по строкам часто даёт более естественные переходы.
FAQ
Что такое Z-функция строки простыми словами? Для каждой позиции - длина куска, который начинается в и совпадает с началом строки. Если , то на позиции 4 строка снова стартует с - те же три первых символа. Значит .
Чем Z-функция отличается от префикс-функции ? смотрит на подстроку и измеряет, насколько её собственный префикс совпадает с её суффиксом. фиксирует позицию и смотрит, как далеко вперёд тянется совпадение с префиксом всей строки. Обе строятся за и взаимовыразимы.
Как через Z искать подстроку в тексте ? Склеить s = P + \ + T$siTz[i]P$, - начало вхождения. Время - линейное от суммарной длины.
Коротко
Z-функция - длина наибольшего префикса строки , совпадающего с подстрокой, начинающейся в позиции . Строится за с одним указателем «правого края» самого дальнего Z-блока: внутри блока используем уже посчитанное зеркальное значение, вне - стартуем сравнение с нуля. Амортизация - на монотонном росте . Через Z за линейное время решаются поиск подстроки (конкатенация P\T$), подсчёт периодов и числа различных подстрок, проверка границ. С префикс-функцией KMP взаимовыразима - выбор между ними обычно вопрос привычки и читаемости кода.
Читайте также

Алгоритм Бойера-Мура-Хорспула: как работает упрощённый BM
Алгоритм Бойера-Мура-Хорспула простыми словами: одна таблица сдвигов по последнему символу окна, среднее время O(n/m), худший случай и сравнение с BM, KMP и Sunday.

Алгоритм Рабина-Карпа: поиск подстроки за O(n+m)
Разбираем алгоритм Рабина-Карпа: как полиномиальный хеш и скользящее окно ускоряют поиск подстроки до O(n+m) в среднем, почему бывают ложные совпадения и при чём тут плагиат.

Алгоритм Кнута-Морриса-Пратта: поиск подстроки за O(n+m)
Алгоритм Кнута-Морриса-Пратта ищет подстроку без возвратов по тексту. Разбираем префикс-функцию по шагам, оцениваем сложность и смотрим понятный пример работы.