Вообще конечный автомат - это довольно простой концепт. Это, грубо говоря, таблица с инструкциями, где записано, в какое состояние должен перейти автомат из текущего состояния, при условии внешней команды (или другого внешнего воздействия).
В принципе, при переходе автомат может выполнять какие-то действия.
Вот пример автомата, который проверяет, делится ли число в двоичной системе счисления на три:
Автомат описывает следующая схема (схемы нагляднее таблиц, но конвертируемы):
Признак делимости на три бинарного числа:
(это сложный автомат, вы можете поразбираться, почему он работает, если хотите)
Почситать количество ненулевых битов на нечетных и четных позициях. Если разность делится на три, то и само число делится на три.
Если строка не проходит по автомату, она "отклоняется".
Далее, автомат можно представить в виде таблицы:
0 1
q0 | q0 | q1 |
q1 | q2 | q0 |
q2 | q1 | q2 |
Итак, здесь конечный автомат:
Конченый автомат, обозначаемый далее , содержит в себе пять элементов:
Замечания:
Эта функция вычисляет состояние автомата после прочтения строки . Определение функции рекурсивно:
здесь и далее в тексте запись формата , и так далее обозначают передачу в функцию в качестве аргумента конкатенацию строки и символа .
Конечный автомат для поиска подстрок создается при помощи функции суффиксов .
Допустим, после чтения строки автомат оказался в состоянии . Функция устроена таким образом, чтобы после чтения номер полученного состояния соответствовал бы длине наибольшего префикса .
То есть для полученного , (или ) является наибольшим префиксом .
И автомат начинает сравнение следующего символа текста с того символа строки, на которое указывает состояние .
Для паттерна GAGAGTT
Вариант сдвига #1:
Переход:
Вариант сдвига #2:
Переход (используется один из "тривиальных" переходов в начальное состояние):
Автомат после чтения нового символа должен перейти в состояние с максимальным префиксом паттерна , который при этом еще и суффикс : это , и одновременно .
Есть два варианта того, что может случиться при переходе:
</font>
w = "010101"
Или таблицы перехода, если угодно
Очень медленная функция, ):
ComputeDelta(pattern, Sigma): 1 m = Length(pattern) 2 for q = [0..m]: 3 for a in Sigma: -- каждый символ алфавита 4 k = Min(m + 1, q + 2) 5 repeat 6 k -= 1 7 until P[:k] is prefix of P[:q] ++ a -- P[:q] ++ a - конкатенация P[:q] и a 8 delta(q,a) = k -- запоминаем значение в таблицу 9 return delta -- можно воспринимать как двумерную таблицу (N_states + 1, |Sigma|)
Можно ускорить работу до ), но для этого нужно использовать некоторые хитрости, которые используются в алгоритме Кнута-Морриса-Пратта.
После составления конечного автомата сам алгоритм действует максимально прямолинейно:
FAMatcher(text, delta, m): 1 -- В автомат передается не паттерн, а сразу функция delta, а так же m - длина паттерна 2 n = Length(text) 3 matches = new empty List 4 q = 0 -- начальное состояние 5 for i = [0..n): 6 q = delta(q, text[i]) 7 if q == m: 8 matches.append(i - m + 1) 9 return matches
</font>
Алгоритм был разработан в 1973 (?) году Кнутом и Праттом, и независимо Моррисом. Опубликован совместно.
Это был первый алгоритм сравнения строк, работающий за в худшем случае, при этом алгоритм требует всего дополнительной памяти (сверх той, которая и так занята текстом и паттерном).
Сложность алгоритма никак не зависит от размера алфавита (с оговоркой, что сравнение двух элементов алфавита выполняется за )
Вообще, направление неважно. Если паттерн содержится в тексте, неважно, в каком направлении писался текст - важно, что совпадение есть
(Да, почти такая же иллюстрация была раньше. Но принцип очень похож)
Что мы наблюдаем?
Интуитивно, из последнего и следует линейность по длине текста
Что с самоповторением паттерна?
где - префикс паттерна длины .
Префикс был отброшен, так как он меньше, чем префикс
</font>
Возможно, это наиболее сложная часть алгоритма KMP, в частности, доказательство того, что обработка паттерна завершается за .
"Наивный" алгоритм с временем более линейного довольно прост и мало отличается от "наивного" алгоритма сравнения строк.
Более "хитрый" подход использует для составления таблицы ту информацию, которая была вычислена ранее и сохранена в таблие.
ComputePrefix(pattern): 1 m = Length(pattern) 2 table = new Array of zeros of Length(m) 3 k = 0 -- сколько символов префикса совпало 4 for q in [1..m): -- обход всех символов в паттерне 5 while k > 0 and pattern[k] != pattern[q]: 6 k = table[k-1] 7 if pattern[k] == pattern[q]: -- эта часть вне while 8 k += 1 9 table[q] = k 10 return table
if pattern ... k += 1while, используется старая информация из таблицы (известно, что похожий суффикс был)</font>
Алгоритм Кнута-Морриса-Пратта, с учетом готовой таблицы, довольно "прямолинеен".
KMP(text, pattern): 1 m = Length(pattern) 2 n = Length(text) 3 table = ComputePrefix(pattern) 4 q = 0 -- сколько символов совпало 5 matches = new empty List 6 for i in [0..n): -- обход всех символов в тексте 7 while q > 0 and pattern[q] != text[i]: 8 q = table[q-1] 9 if pattern[q] == text[i]: -- эта часть вне while 10 q += 1 11 if pattern[q] == m: -- совпали все символы паттерна 12 matches.append(i - m + 1) -- начало выравнивания 13 q = table[q-1] 14 return matches
Вычисление префикса
ComputePrefix(pattern): 1 m = Length(pattern) 2 table = new Array of zeros of Length(m) 3 k = 0 -- сколько символов префикса совпало 4 for q in [1..m): -- обход всех символов в паттерне 5 while k > 0 and pattern[k] != pattern[q]: 6 k = table[k-1] 7 if pattern[k] == pattern[q]: -- эта часть вне while 8 k += 1 9 ...... и сам KMP:
KMP(text, pattern): 1 m = Length(pattern) 2 n = Length(text) 3 table = ComputePrefix(pattern) 4 q = 0 -- сколько символов совпало 5 -- matches = new empty List -- сейчас не важна 6 for i in [0..n): 7 while q > 0 and pattern[q] != text[i]: 8 q = table[q-1] 9 if pattern[q] == text[i]: 10 q += 1 11 ...
если заменить в первом случае pattern на text, код станет практически неотличимым.
</font>
</font>