Appearance
Префикс-функция, КМП и конечные автоматы
1. Префикс-функция
Определение. Для строки
Пример для строки abacaba:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | |
|---|---|---|---|---|---|---|---|
| a | b | a | c | a | b | a | |
| 0 | 0 | 1 | 0 | 1 | 2 | 3 |
: у строки из одного символа нет собственных префиксов (по определению значение не определено, то есть ставим 0) : в abaпрефиксaсовпадает с суффиксомa.: cне совпадает ни с чем в начале.: в abacabaпрефиксabaсовпадает с суффиксомaba, и это максимальное совпадение.
Отличие от Z-функции
Они родственные, но смотрят в разные стороны:
— про суффикс, заканчивающийся в позиции (смотрим назад); — про подстроку, начинающуюся в позиции (смотрим вперед): сколько символов от совпадает с началом строки.
На abacaba в позиции 4 значение aba совпадает с началом), а
Как строить за линейное время
Идем слева направо. Пусть
- Берем
— длину текущего «лучшего» префикса, совпадающего с суффиксом перед позицией . - Сравниваем символ
(следующий за префиксом) и (новый символ) - Равны -> префикс удлиняется:
. Больше ничего делать не надо. - Не равны -> надо найти следующий по длине суффикс, который тоже является префиксом. Логика похожа на то, что было в Ахо-Корасике: «откусываем» от суффикса, пока не найдем тот, к которому можно дописать символ. Переходим
и повторяем сравнение. Если дошли до и , то .
- Равны -> префикс удлиняется:
Почему прыжок на
Пример с откатом. aabaaab:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | |
|---|---|---|---|---|---|---|---|
| a | a | b | a | a | a | b | |
| 0 | 1 | 0 | 1 | 2 | 2 | 3 |
В позиции 5 имеем b дописывается к префиксу aa:
Код
python
def prefix_function(s: str) -> list[int]:
n = len(s)
p = [0] * n
for i in range(1, n): # p[0] не определено, начинаем с 1
j = p[i - 1]
while j > 0 and s[i] != s[j]:
j = p[j - 1] # прыжок к следующему подходящему суффиксу
if s[i] == s[j]:
j += 1
p[i] = j
return pЗамечания:
- Цикл
whileне бесконечный:строго убывает, а при цикл останавливается. Индекс в нем не меняется — мы подбираем длину префикса для одной и той же позиции. - Сравниваем
s[i]сs[j], а прыгаем поp[j - 1]. Здесь— это длина префикса, поэтому следующий за ним символ имеет индекс , а последний символ самого префикса — .
Сложность
Суммарно while его уменьшает, поэтому всего шагов while не больше
2. Поиск подстроки
Задача. Даны паттерн
2.1. Решение через склейку
Строим строку # — символ, которого нет ни в паттерне, ни в тексте (разделитель). Считаем префикс-функцию для всей
Пример: aba#abacaba:
a b a # a b a c a b a
0 0 1 0 1 2 3 0 1 2 3Тройки соответствуют вхождениям. Разделитель нужен, чтобы значения
Решение работает за
2.2. Алгоритм Кнута–Морриса–Пратта
КМП строит префикс-функцию только для паттерна, а потом идет по тексту и использует ее как минимальный автомат
Два указателя:
— позиция в тексте (просто идет слева направо); — сколько символов паттерна сейчас совпало (позиция в паттерне).
На каждом символе текста:
- Пока
и — откатываемся: . Мы не сбрасываемся на начало, а оставляем максимальный суффикс, который все еще является префиксом паттерна: потерять его раньше времени было бы неправильно. - Если
, то . - Если
, паттерн найден целиком: вхождение начинается в позиции . Дальше нужно откатиться тем же способом ( ), чтобы найти перекрывающиеся вхождения.
python
def kmp(s: str, t: str) -> list[int]:
"""Все вхождения паттерна t в строку s (индексы начала)."""
m = len(t)
if m == 0: # пустой паттерн — искать нечего
return []
p = prefix_function(t) # префикс-функция ТОЛЬКО по паттерну
res = []
j = 0
for i, ch in enumerate(s):
while j > 0 and ch != t[j]:
j = p[j - 1]
if ch == t[j]:
j += 1
if j == m: # дошли до конца паттерна
res.append(i - m + 1)
j = p[j - 1] # откат, чтобы не потерять перекрывающиеся
return resПример.
| Позиция | Символ | Что происходит | |
|---|---|---|---|
| 0 | a | совпало | 1 |
| 1 | b | совпало | 2 |
| 2 | a | совпало, вхождение с 0; откат | 1 |
| 3 | c | 0 | |
| 4 | a | совпало | 1 |
| 5 | b | совпало | 2 |
| 6 | a | совпало, вхождение с 4; откат | 1 |
| 7 | b | совпало (суффикс a не потерян) | 2 |
| 8 | a | совпало, вхождение с 6; откат | 1 |
Вхождения: 0, 4, 6. Если бы после нахождения aba мы сбросились в начало, вхождение с 6 потерялось бы (a в конце первой aba — начало следующей).
2.3. Сложность
Главное отличие от склейки: КМП не хранит префикс-функцию для всего текста — только
3. Автомат: ДКА для поиска подстроки
Детерминированный конечный автомат (ДКА) — это граф, в котором:
- есть начальное состояние (с него начинаем);
- есть терминальные (принимающие) состояния: попали в них — значит, строка подошла;
- из каждого состояния по каждому символу алфавита ведет ровно один переход.
Автомату на вход подается строка
3.1. Автомат, который ищет aba (только первое вхождение)
Состояние aba». Состояния 0, 1, 2, 3; терминальное — 3.
- Из 0: по
aв 1, поbостаемся в 0. - Из 1: по
bв 2; поa(дваaподряд) остаемся в 1: последняяaможет быть началом ответа. - Из 2: по
aв 3 (нашлиaba); поb(получилосьabb) в 0: ни один суффикс не подходит как префикс. - Из 3 (терминальное): мы замыкаем переходы на себя, чтобы автомат не «сломался», хотя можно оставить и так, чтобы ломался.
Такой автомат отвечает на вопрос «встречается ли aba вообще», но не скажет, где еще оно встречается: после попадания в терминальную вершину мы остаемся в ней, сколько бы букв ни пришло.
3.2. Автомат КМП — ищет все вхождения
Чтобы находить все вхождения, из терминальной вершины надо уметь откатываться:
- из 3 по
b→ в 2 (abab: суффиксab— префикс паттерна); - из 3 по
a→ в 1 (abaa: суффиксa— префикс паттерна).
То есть автомат из пункта 3.1 дополняем переходами, которые «подхватывают» суффикс, уже совпавший с началом паттерна. Это и есть автомат КМП.
Итого переходы:
| Состояние | по a | по b |
|---|---|---|
| 0 | 1 | 0 |
| 1 | 1 | 2 |
| 2 | 3 | 0 |
| 3 (терминальное) | 1 | 2 |
3.3. Связь с алгоритмом КМП
Алгоритм КМП и автомат КМП — одно и то же, просто в разном представлении:
- в автомате хранятся все переходы по всему алфавиту
- в КМП мы упаковываем автомат в массив
и храним только нужные переходы: переход вперед («совпало», ) и «откат назад» по , который дает максимальный суффикс, равный префиксу.
Вместо того чтобы сразу прыгнуть в нужное состояние по любой букве, КМП делает серию откатов
Префикс-функция в Ахо-Корасике и в КМП — родственные идеи: суффиксные ссылки Ахо-Корасика — это обобщение откатов по