E098. Z-функция строки и её вычисление

e-maxx algorithm original: C/C++ #algorithm #emaxx #kmp #string
El texto de la tarea se traduce del ruso para el idioma seleccionado. El código no cambia.

Источник: 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м

. Сравним это

начальное значение

с величиной

, получаем три варианта:

H

Докажем, что в этом случае ни одной итерации цикл

не сделает.

Это легко доказать, наEjemplo, от противного: если бы цикл

сделал хотя бы одну итерацию, это бы означало,

что определённое нами значение

было неточным, меньше настоящей длины совпадения. Но т.к. строки

и

совпадают, то это означает, что в позиции

стоит неправильное значение: меньше, чем

должно быть.

Таким образом, в этом варианте из корректности значения

и из того, что оно меньше

, следует,

что это значение совпадает с искомым значением

.

H

В этом случае цикл

может совершить несколько итераций, однако каждая из них будет приводить к

увеличению нового значения

на единицу: потому что первым же сравниваемым символом будет

,

который вылазит за пределы отрезка

.

H

Этот вариант принципиально невозможен, в силу определения

. Таким образом, мы доказали, что каждая итерация вложенного цикла приводит к продвижению указателя вправо. Т.к.

не могло оказаться больше

, это означает, что всего этот цикл сделает не более

итерации.

Поскольку вся остальная часть Algoritmoа, очевидно, работает за

, то мы доказали, что и весь Algoritmo

вычисления Z-функции выполняется за линейное время.

Applications

Рассмотрим несколько применений Z-функции при решении конкретных задач. Applications эти будут во многом аналогичным Applicationsм префикс-функции.

Поиск подстроки в строке

Во избежании путаницы, назовём одну строку текстом

, другую — образцом

. Таким образом,

Tarea заключается в том, чтобы find все вхождения образца

в текст

.

Для решения этой задачи образуем строку

, т.е. к образцу припишем текст через символ-

разделитель (который не встречается нигде в самих cadenaх).

Посчитаем для полученной строки Z-функцию. Тогда для любого

в отрезке

по

соответствующему значению

можно понять, Entradaит ли образец

в текст

, начиная с

позиции

: если это значение Z-функции равно

, то да, Entradaит, иначе — нет.

Таким образом, Asymptotic complexity решения получилась

. Потребление памяти имеет ту

же асимптотику.

Количество различных подстрок в строке

Дана cadena

длины

. it is required посчитать количество её различных подстрок. Будем решать эту задачу итеративно. А именно, научимся, зная текущее количество различных подстрок, пересчитывать это количество при добавлении в конец одного символа.

Итак, пусть

— текущее количество различных подстрок строки

, и мы добавляем в конец символ

. Очевидно,

в результате могли появиться некоторые новые подстроки, оканчивавшиеся на этом новом символе

(а именно,

все подстроки, оканчивающиеся на этом символе, но не встречавшиеся раньше).

Возьмём строку

и инвертируем её (запишем символы в обратном порядке). Наша Tarea —

посчитать, сколько у строки

таких префиксов, которые не встречаются в ней более нигде. Но если мы посчитаем

для строки

Z-функцию и найдём её максимальное значение

, то, очевидно, в строке

встречается (не в начале)

её префикс длины

, но не большей длины. Понятно, префиксы меньшей длины уже точно встречаются в ней. Итак, мы получили, что number новых подстрок, появляющихся при дописывании символа

, равно

, где

— текущая длина строки после приписывания символа

.

Следовательно, Asymptotic complexity решения для строки длины

составляет

. Стоит заметить, что совершенно аналогично можно пересчитывать за

количество различных подстрок и

при дописывании символа в начало, а также при удалении символа с конца или с начала.

Сжатие строки

Дана cadena

длины

. it is required find самое короткое её "сжатое" представление, т.е. find такую строку

наименьшей длины, что

можно представить в виде конкатенации одной или нескольких копий .

Для решения посчитаем Z-функцию строки

, и найдём первую позицию

такую, что

, и при этом

делится на

. Тогда строку

можно сжать до строки длины

. Prueba такого решения практически не отличается от доказательства решения с помощью префикс-функции.

Задачи в online judges

Список задач, которые можно решить, используя Z-функцию:

● UVA #455 "Periodic Strings" [Complexity: средняя]

● UVA #11022 "String Factoring" [Complexity: средняя]

C# solución

borrador automático, revisar antes de enviar
using System;
using System.Collections.Generic;
using System.Linq;

public static class AlgorithmDraft
{
    // Auto-generated C# draft from the original e-maxx C/C++ listing. Review before production use.
    List<int> z_function_trivial (string s) {
            int n = (int) s.length();
            List<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;
    }
    List<int> z_function (string s) {
            int n = (int) s.length();
            List<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;
    }
}

C++ solución

coincidente/original
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;
}
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;
}

Java solución

borrador automático, revisar antes de enviar
import java.util.*;
import java.math.*;

public class AlgorithmDraft {
    // Auto-generated Java draft from the original e-maxx C/C++ listing. Review before production use.
    ArrayList<Integer> z_function_trivial (string s) {
            int n = (int) s.length();
            ArrayList<Integer> 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;
    }
    ArrayList<Integer> z_function (string s) {
            int n = (int) s.length();
            ArrayList<Integer> 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;
    }
}

Материал разбит как Algoritmoическая Tarea: изучить постановку, понять асимптотику и реализовать Algoritmo на выбранном языке.

Vacantes para esta tarea

Se muestran vacantes activas con etiquetas coincidentes.

Todas las vacantes
Todavía no hay vacantes activas.