EssayAI
Блог
Блог
Математика и алгоритмы

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

23 февраля 2026Время чтения: 9 минут
#Z-функция#алгоритмы строк#префиксная функция#поиск подстроки#линейные алгоритмы
Z-функция строки за O(n): построение и применение

Z-функция - компактная характеристика строки, на которой строится едва ли не половина олимпиадного арсенала по работе с текстами: поиск подстроки, подсчёт различных подстрок, определение периодов. Идея простая: для каждой позиции ii хранится длина наибольшего префикса исходной строки, который начинается именно отсюда. Полный массив строится за O(n)O(n) с одним указателем «правого края уже изученной области».

Определение z[i]z[i] через максимальный префикс

Пусть ss - строка длины nn, индексация с нуля. Z-функция - это массив zz той же длины, где

z[i]=max{k0:s[0..k1]=s[i..i+k1]}.z[i] = \max\{k \ge 0 : s[0..k{-}1] = s[i..i{+}k{-}1]\}.

Словами: z[i]z[i] - длина наибольшего префикса ss, совпадающего с подстрокой, начинающейся в позиции ii. По соглашению z[0]z[0] либо не определяется, либо полагается равным nn (вся строка - префикс самой себя) - на алгоритм это никак не влияет, главное - не использовать z[0]z[0] в применениях.

Простой пример: для s=aabcaabs = \text{aabcaab}

ii0123456
s[i]s[i]aabcaab
z[i]z[i]-100310

z[4]=3z[4] = 3, потому что с позиции 4 идёт aab\text{aab} - это первые 3 символа ss. z[1]=1z[1] = 1: совпадает только a\text{a}, дальше второй символ ss - это a\text{a}, а s[2]=bs[2] = \text{b}. Всё остальное - нули, потому что соответствующие символы не равны s[0]s[0].

Отличие от префикс-функции

Z-функция и префикс-функция π\pi из алгоритма Кнута-Морриса-Пратта родственны, но смотрят на строку с разных концов. π[i]\pi[i] - длина наибольшего собственного префикса подстроки s[0..i]s[0..i], который одновременно является её суффиксом: фокус на отрезке от 0 до ii, и совпадение проверяется в конце. z[i]z[i] - фокус на позиции ii и проверяется, как далеко вперёд тянется совпадение с префиксом всей строки.

Формально обе функции взаимовыразимы за O(n)O(n): по π\pi можно восстановить zz и наоборот. На практике Z-функция чаще проще в реализации (меньше тонких case'ов с while-циклом по π\pi), а π\pi удобнее, когда нужны именно «откаты» в KMP-стиле. Многие задачи решаются обеими - выбор сводится к привычке.

Наивный O(n2)O(n^2) и идея ускорения

Наивный алгоритм - посимвольно сравнивать s[0..]s[0..] и s[i..]s[i..], пока совпадает. Худший случай - строка из одинаковых символов aaaa...a\text{aaaa...a}: каждое z[i]=niz[i] = n - i, суммарно Θ(n2)\Theta(n^2) операций.

Ускорение опирается на наблюдение: если мы уже посчитали z[j]z[j] для j<ij < i и знаем самый правый «Z-блок» [l,r][l, r], где r=l+z[l]1r = l + z[l] - 1 - конец совпавшего префикса, - то значения внутри блока можно частично восстановить без сравнений символов. Подстрока s[l..r]s[l..r] совпадает с s[0..rl]s[0..r-l], и любую позицию ii внутри [l,r][l, r] можно сопоставить с зеркальной ili - l в начале строки и использовать z[il]z[i - l] как нижнюю оценку.

Алгоритм по шагам с двумя случаями

Поддерживаем пару (l,r)(l, r) - самый правый Z-блок, изначально l=r=0l = r = 0. Для каждого ii от 1 до n1n - 1:

Случай 1: i>ri > r. Z-блок нас не покрывает, ничего полезного из прошлого не вытащить - стартуем «голое» сравнение s[0..]s[0..] и s[i..]s[i..] с нуля и увеличиваем z[i]z[i], пока символы совпадают. После - обновляем l=il = i, r=i+z[i]1r = i + z[i] - 1, если z[i]>0z[i] > 0.

Случай 2: iri \le r. Внутри блока. Зеркало j=ilj = i - l, и мы знаем z[j]z[j]. Возможны две ветки:

  • z[j]<ri+1z[j] < r - i + 1 - зеркальное значение меньше, чем расстояние до правой границы. Тогда z[i]=z[j]z[i] = z[j] железно: дальше тянуть нельзя, потому что в зеркальной позиции совпадение уже оборвалось, а у нас та же подстрока.
  • z[j]ri+1z[j] \ge r - i + 1 - зеркало упирается в границу, и за неё мы не знаем, что там. Берём нижнюю оценку z[i]=ri+1z[i] = r - i + 1 и пытаемся «дотянуть» сравнением символов начиная с позиции r+1r + 1. Если получилось - обновляем rrl=il = i).

Псевдокод:

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

Шесть строк - весь алгоритм. Никаких массивов алфавита, никаких отдельных билдеров.

Сложность O(n)O(n) через амортизированный анализ

Внешний цикл - nn итераций. Сравнения символов делаются только в while, и каждое успешное сравнение увеличивает rr как минимум на единицу. Указатель rr монотонно растёт и не превосходит n1n - 1 - всего успешных сравнений не больше nn. Неуспешных - тоже не больше nn: по одному на итерацию внешнего цикла. Итого все сравнения суммарно O(n)O(n), плюс nn административных операций - O(n)O(n) времени и O(n)O(n) памяти под массив. Анализ почти дословно повторяет KMP: «работа равна суммарному движению одного указателя».

Применения

**Поиск подстроки PP в TT через P\T.Берём.** Берём s = P + $ + T,где, где $символ,невстречающийсянив- символ, не встречающийся ни вP,нив, ни в T.Считаем. Считаем zнанаs.Каждаяпозиция. Каждая позиция iвчастив частиT,где, где z[i] = |P|,началовхождения.Время, - начало вхождения. Время O(|P| + |T|)$ - функционально эквивалентно KMP, реализация короче.

Число различных подстрок. Добавляя по одному символу в конец строки ss длины kk, считаем Z-функцию для развёрнутой ss' длины k+1k + 1 - новых подстрок добавляется k+1maxz[i]k + 1 - \max z[i]. Просуммировав, получаем ответ за O(n2)O(n^2). Не самый быстрый способ (суффиксный массив даёт O(nlogn)O(n \log n)), но самый простой.

Периоды строки. kk - период ss, если s[i]=s[i+k]s[i] = s[i + k] для всех ii. Эквивалентно: z[k]nkz[k] \ge n - k. Перебрав все kk, за линейное время находим все периоды.

Палиндромы через комбинацию с Манакером. Z-функция для s+#+reverse(s)s + \# + \text{reverse}(s) даёт палиндромные префиксы. Сам алгоритм Манакера работает быстрее, но в задачах с дополнительными ограничениями такой гибрид иногда удобнее.

Сравнение с KMP

Z-функция и префикс-функция π\pi - две стороны одной медали. Из π\pi строится zz за O(n)O(n) и наоборот. Обе работают за линию, обе используют амортизированный анализ через один монотонный указатель.

Различия - в форме изложения. z[i]z[i] описывает «что начинается в позиции ii»; π[i]\pi[i] - «что заканчивается в s[0..i]s[0..i]». Олимпиадные шаблоны чаще берут Z - за десяток строк кода и однозначную интерпретацию. KMP остаётся стандартом в учебниках и в реализациях String.indexOf.

Типовые задачи (Codeforces)

  • CF 126B Password. Самый длинный префикс, который встречается ещё как минимум один раз и в середине, и в конце. Префикс длины kk - суффикс, если z[nk]=kz[n-k] = k; встречается в середине - если max1i<nkz[i]k\max_{1 \le i < n-k} z[i] \ge k.
  • CF 432D Prefixes and Suffixes. Все префиксы строки, которые одновременно являются суффиксами, и число вхождений каждого. Z-функция даёт это в одну итерацию.
  • CF 119D String Transformation. Минимальная циклическая ротация - через Z или Booth, обе линейные.

Частые ошибки

  • Используют z[0]z[0] как полноценное значение. z[0]=nz[0] = n по соглашению, но в большинстве задач его явно исключают: «префикс совпадает сам с собой» - тривиальный факт, не несущий информации.
  • **Забывают про разделитель \$$ при поиске PввT.БезнегоZфункцияможет«протянуть»совпадениеиз.** Без него Z-функция может «протянуть» совпадение из PввTидатьложноезначениеи дать ложное значениеz[i] > |P|$ - потом непонятно, что считать вхождением.
  • Сравнивают z[il]z[i - l] с rir - i вместо ri+1r - i + 1. Off-by-one: блок [l,r][l, r] включает обе границы, расстояние до конца - ri+1r - i + 1, а не rir - i.
  • Пытаются обновить (l,r)(l, r) при z[i]=0z[i] = 0. Если ни одного символа не совпало, обновлять блок нельзя - иначе rr может уменьшиться.
  • Считают, что Z-функция полностью эквивалентна KMP по сложности и поэтому взаимозаменяема в любой задаче. Для одних задач удобнее zz, для других π\pi; в задачах с DP по строкам π\pi часто даёт более естественные переходы.

FAQ

Что такое Z-функция строки простыми словами? Для каждой позиции ii - длина куска, который начинается в ii и совпадает с началом строки. Если s=aabcaabs = \text{aabcaab}, то на позиции 4 строка снова стартует с aab\text{aab} - те же три первых символа. Значит z[4]=3z[4] = 3.

Чем Z-функция отличается от префикс-функции π\pi? π[i]\pi[i] смотрит на подстроку s[0..i]s[0..i] и измеряет, насколько её собственный префикс совпадает с её суффиксом. z[i]z[i] фиксирует позицию ii и смотрит, как далеко вперёд тянется совпадение с префиксом всей строки. Обе строятся за O(n)O(n) и взаимовыразимы.

Как через Z искать подстроку PP в тексте TT? Склеить s = P + \ + Tчерезразделительчерез разделитель$,которогонетниводнойизстрок.ПосчитатьZфункциюна, которого нет ни в одной из строк. Посчитать Z-функцию на s.Каждаяпозиция. Каждая позиция iвчастив частиT,где, где z[i]равнодлинеравно длинеP$, - начало вхождения. Время - линейное от суммарной длины.

Коротко

Z-функция z[i]z[i] - длина наибольшего префикса строки ss, совпадающего с подстрокой, начинающейся в позиции ii. Строится за O(n)O(n) с одним указателем «правого края» rr самого дальнего Z-блока: внутри блока используем уже посчитанное зеркальное значение, вне - стартуем сравнение с нуля. Амортизация - на монотонном росте rr. Через Z за линейное время решаются поиск подстроки (конкатенация P\T$), подсчёт периодов и числа различных подстрок, проверка границ. С префикс-функцией KMP взаимовыразима - выбор между ними обычно вопрос привычки и читаемости кода.

Доверьте текст нейросети EssayAI

Открыть EssayAI

Бесплатно, на русском языке и без VPN

Читайте также