Префикс-функция

Рассмотрим строку . Будем обозначать подстроку строки , которая начинается с -го символа и заканчивается символом с индексом . Префикс-функцию от строки определим с помощью массива, который будем обозначать . – это длина наибольшего префикса подстроки , совпадающего с ее суффиксом (но не равного самой подстроке ).

Алгоритм вычисления

Заметим сначала, что превосходит не более чем на единицу, т.к. в противном случае в качестве значения мы бы должны были взять число . Рассмотрим шаг алгоритма с номером . Мы уже знаем значение , обозначим его за . Eсли совпадает с , то , и можно переходить к следующему шагу алгоритма:

.
где – символ строки, – символы которые нужно сравнить (префикс и суффикс могут «накладываться» друг на друга). Если же , то нужно рассмотреть префикс длины . Предположим, что это не так, то есть, что существует префикс подстроки длины , такой что и совпадающий с суффиксом длины :
.
Это невозможно, т.к. в качестве значения мы должны были бы взять . Аналогично, если длина не подходит, то проверять нужно , и т.д.

Реализация алгоритма на языке C++:

std::vector<int> pf(s.size() + 1, 0); for (int i = 2; i < (int)pf.size(); i++) { int prefLen = pf[i - 1]; while (true) { if (s[i - 1] == s[prefLen]) { pf[i] = prefLen + 1; break; } else if (prefLen == 0) { break; } prefLen = pf[prefLen]; } }

На каждом шаге внешнего цикла значение префикс-функции увеличивается не более чем на единицу, поэтому . На каждом шаге внутреннего цикла значение уменьшается не менее, чем на единицу. Отсюда следует, что суммарное количество итераций внутреннего цикла ограничено числом итераций внешнего. Таким образом, сложность алгоритма можно оценить как .

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

Приведем пример задачи, которая решается с помощью префикс-функции. Пусть – это некоторая строка. Требуется найти количество ее различных подстрок.

Будем решать задачу с помощью динамического программирования. На каждом шаге алгоритма будем добавлять в строку по символу из и искать количество получившихся новых подстрок. Рассмотрим шаг алгоритма с номером . Пусть – текущая строка, то есть . Нам нужно найти количество новых подстрок, которые заканчиваются символом . Для этого «перевернем» строку и обозначим ее . Таким образом, задача сводится к поиску суффиксов подстрок , совпадающих с ее префиксом, который теперь начинается с нового символа . Заметим, что достаточно найти только максимальный такой суффикс, т.к. все меньшие суффиксы будут в нем содержаться. Соответственно, количество новых подстрок, полученных на данном шаге, можно будет найти по формуле

.
где – это префикс-функция, вычисленная для . Пусть – это длина строки . Тогда мы раз будем добавлять по символу и каждый раз заново вычислять префикс-функцию. Значит сложность алгоритма составит .

Реализация решения задачи на языке C++:

int nUniqueSubstrings = 0; std::vector<int> pf(s.length() + 1, 0); std::string currentString = ""; for (int i = 0; i < (int)s.length(); i++) { currentString += s[i]; std::string reversedCurrentString = currentString; std::reverse(reversedCurrentString.begin(), reversedCurrentString.end()); pf[1] = 0; for (int j = 2; j <= i + 1; j++) { int prefLen = pf[j - 1]; while (true) { if (reversedCurrentString[j - 1] == reversedCurrentString[prefLen]) { pf[j] = prefLen + 1; break; } else if (prefLen == 0) { pf[j] = 0; break; } prefLen = pf[prefLen]; } } int maxLength = 0; for (int j = 2; j <= i + 1; j++) { maxLength = std::max(maxLength, pf[j]); } nUniqueSubstrings += i + 1 - maxLength; }