E098. Z-функция строки и её вычисление
Источник: e-maxx.ru/algo, страница PDF 287.
Пусть дана cadena
длины
. Тогда Z-функция ("зет-функция") от этой строки — это arreglo длины
,
-ый
element которого равен наибольшему числу символов, начиная с позиции
, совпадающих с первыми символами строки
.
Иными словами,
— это наибольший общий префикс строки
и её
-го суффикса. Примечание. В данной статье, во избежание неопределённости, мы будем считать строку 0-индексированной — т.
е. первый символ строки имеет индекс
, а последний —
.
Первый element Z-функции,
, обычно считают неопределённым. В данной статье мы будем считать, что он равен нулю (хотя ни в Algoritmoе, ни в приведённой реализации это ничего не меняет). В данной статье приводится Algoritmo вычисления Z-функции за время
, а также различные Applications
этого Algoritmoа.
Ejemploы
Приведём для Ejemploа подсчитанную Z-функцию для нескольких строк:
●
:
●
:
●
:
Тривиальный Algoritmo
Формальное Definición можно представить в виде следующей elementарной реализации за :
vector<int> z_function_trivial (string s) {
int n = (int) s.length();
vector<int> z (n);
for (int i=1; i<n; ++i)
while (i + z[i] < n && s[z[i]] == s[i+z[i]])
++z[i];
return z;
}
Мы просто для каждой позиции
перебираем ответ для неё
, начиная с нуля, и до тех пор, пока мы не
обнаружим несовпадение или не дойдём до конца строки. Разумеется, эта Implementación слишком неэффективна, перейдём теперь к построению эффективного Algoritmoа.
Эффективный Algoritmo вычисления Z-функции
Чтобы получить эффективный Algoritmo, будем вычислять значения
по очереди — от
до
, и при
этом постараемся при вычислении очередного значения
максимально использовать уже вычисленные значения.
Назовём для краткости подстроку, совпадающую с префиксом строки
, отрезком совпадения.
НаEjemplo, значение искомой Z-функции
— это длиннейший отрезок совпадения, начинающийся в позиции
(и заканчиваться он будет в позиции
).
Для этого будем поддерживать координаты
самого правого отрезка совпадения, т.е. из
всех обнаруженных отрезков будем хранить тот, который оканчивается правее всего. В некотором смысле, индекс
—
это такая граница, до которой наша cadena уже была просканирована Algoritmoом, а всё остальное — пока ещё не известно. Тогда если текущий индекс, для которого мы хотим посчитать очередное значение Z-функции, — это
, мы имеем один
из двух вариантов:
●
— т.е. текущая позиция лежит за пределами того, что мы уже успели обработать.
Тогда будем искать
тривиальным Algoritmoом, т.е. просто пробуя значения
,
, и т.
д. Заметим, что в итоге, если
окажется
, то мы будем обязаны обновить координаты самого правого
отрезка
— т.к.
гарантированно окажется больше
.
●
— т.е. текущая позиция лежит внутри отрезка совпадения
. Тогда мы можем использовать уже подсчитанные предыдущие значения Z-функции, чтобы
проинициализировать значение
не нулём, а каким-то возможно бОльшим numberм.
Для этого заметим, что подстроки
и
совпадают. Это означает, что в качестве
начального приближения для
можно взять соответствующее ему значение из отрезка
, а
именно, значение
.
Однако значение
могло оказаться слишком большим: таким, что при применении его к позиции
оно
"вылезет" за пределы границы
. Этого допустить нельзя, т.к. про символы правее
мы ничего не знаем, и они
могут отличаться от требуемых. Приведём Ejemplo такой ситуации, на Ejemploе строки:
Когда мы дойдём до последней позиции (
), текущим самым правым отрезком будет
. Позиции
с
учётом этого отрезка будет соответствовать позиция
, ответ в которой равен
. Очевидно, что
таким значением инициализировать
нельзя, оно совершенно некорректно. Максимум, каким значением мы
могли проинициализировать — это
, поскольку это наибольшее значение, которое не вылазит за пределы отрезка .
Таким образом, в качестве начального приближения для
безопасно брать только такое выражение:
Проинициализировав
таким значением
, мы снова дальше действуем тривиальным Algoritmoом
— потому что после границы
, вообще говоря, могло обнаружиться продолжение отрезка совпадение, предугадать которое одними лишь предыдущими значениями Z-функции мы не могли. Таким образом, весь Algoritmo представляет из себя два случая, которые фактически различаются
только начальным значением
: в первом случае оно полагается равным нулю, а во втором
— определяется по предыдущим значениям по указанной формуле. После этого обе ветки Algoritmoа сводятся к выполнению тривиального Algoritmoа, стартующего сразу с указанного начального значения. Algoritmo получился весьма простым. Несмотря на то, что при каждом
в нём так или иначе выполняется
тривиальный Algoritmo — мы достигли существенного прогресса, получив Algoritmo, работающий за линейное время. Почему это так, рассмотрим ниже, после того, как приведём реализацию Algoritmoа.
Implementación
Implementación получается весьма лаконичной:
vector<int> z_function (string s) {
int n = (int) s.length();
vector<int> z (n);
for (int i=1, l=0, r=0; i<n; ++i) {
if (i <= r)
z[i] = min (r-i+1, z[i-l]);
while (i+z[i] < n && s[z[i]] == s[i+z[i]])
++z[i];
if (i+z[i]-1 > r)
l = i, r = i+z[i]-1;
}
return z;
} Прокомментируем эту реализацию. Всё Solución оформлено в виде функции, которая по строке returns arreglo длины — вычисленную Z-функцию.
arreglo
изначально заполняется нулями. Текущий самый правый отрезок совпадения полагается равным , т.
е. заведомо маленький отрезок, в который не попадёт ни одно
.
Внутри цикла по
мы сначала по описанному выше Algoritmoу определяем начальное значение
— оно либо останется нулём, либо вычислится на основе приведённой формулы. После этого выполняется тривиальный Algoritmo, который пытается увеличить значение
настолько, насколько
это возможно. В конце выполняется обновление текущего самого правого отрезка совпадения
, если, конечно, это
обновление it is required — т.е. если
.
Asymptotic complexity Algoritmoа
Докажем, что приведённый выше Algoritmo работает за линейное относительно длины строки время, т.е. за . Prueba очень простое.
Нас интересует вложенный цикл
— т.к. всё остальное — лишь константные операции, выполняемые
раз.
Покажем, что каждая итерация этого цикла
приведёт к увеличению правой границы
на единицу. Для этого рассмотрим обе ветки Algoritmoа:
●
В этом случае либо цикл
не сделает ни одной итерации (если
), либо же сделает несколько
итераций, продвигаясь каждый раз на один символ вправо, начиная с позиции
, а после этого — правая граница
обязательно обновится.
Поскольку
, то мы получаем, что действительно каждая итерация этого цикла увеличивает новое значение
на единицу.
●
В этом случае мы по приведённой формуле инициализируем значение
некоторым numberм
. Сравним это
начальное значение
с величиной
, получаем три варианта: