1 StrCmpNaive(text, pattern): 2 lp = length(pattern) 3 lt = length(text) 4 matches = new List 5 for i in range 0..lt exclusively // что здесь не так? 6 for j in range 0..lp exclusively 7 if text[i+j] != pattern[j] 8 break 9 if j == lp - 1 10 matches.append(i) 11 return matches> Ассимптотическая сложность в наихудшем случае = > Какой паттерн (и для какого текста) будет приводить к максимально неэффективной работе алгоритма? (* в случае, если совпавшие строки _не перекрываются_) > Например, поиск `'aaaaaaab'` в `'aaaaaaaaaaaaaaaaaaaaaa'` и тому подобное. Или поиск паттернов из повторяющихся символов, если допускается частичное пересечение разных выравниваний. К примеру, поиск `'aaaa'` в `'aaaaaaaaaaaaaaaaa'` # Мы можем лучше Мы будем предполагать следующее: * Паттерн (в большинстве случаев) значительно меньше, чем сам текст * Алфавит ограничен, и его мы тоже знаем, естественно. Алфавит - множество символов, из которых состоят текст и паттерн Одна из (относительно) простых идей, которую можно использовать для ускорения поиска: > Если встреченный в тексте не совпадающий с паттерном символ вообще не встречается в паттерне, можно смело "проматывать" начало паттерна и располагать его сразу за этим символом
1 StrCmpLessNaive(text, pattern): 2 lp = Length(pattern) 3 lt = Length(text) 4 matches = new List 5 preprocessed = Preprocess(pattern) 6 7 shift = 0 8 while shift < lt - lp + 1: 9 j = 0 10 while j < lp and text[shift+j] == pattern[j]: 11 j += 1 12 if j == lp: 13 matches.append(shift) 14 shift += 1 15 else: 16 shift += UsePreprocessed(text[shift+j], shift, preprocessed) 17 return matches
Возникают следующие вопросы:
shift = 0
HERE IS A SIMPLE EXAMPLE EXAMPLE ^
j = 0; shift = shift + j + 1
shift = 1
HERE IS A SIMPLE EXAMPLE EXAMPLE ^`j = 1; shift = shift + j + 1` `shift = 3`
HERE IS A SIMPLE EXAMPLE
EXAMPLE
^
`j = 1; shift = shift + j + 1`
`shift = 5`
> И так далее. Пока решение выглядит очень сырым. Как можно _существенно_ улучшить его?
Чтобы ускорить обработку последовательностей еще сильнее, нам придется взглянуть на проблему с другой стороны - буквально.
Правда, для этого придется немного переписать основной алгоритм.
## Сравнение строк с (правого) конца паттерна
1 StrCmpReversed(text, pattern): 2 lp = Length(pattern) 3 lt = Length(text) 4 matches = new List 5 preprocessed = Preprocess(pattern) 6 7 shift = 0 8 while shift < lt - lp + 1: 9 j = lp - 1 10 while j >= 0 and text[shift+j] == pattern[j]: 11 j -= 1 12 if j <= 0: 13 matches.append(shift) 14 shift += 1 15 else: 16 if text[j+shift] not in preprocessed == 0: 17 shift += (j + 1) 18 else: 19 shift += 1 20 return matches
shift = 0
HERE IS A SIMPLE EXAMPLE
EXAMPLE
^
j = 6; shift = shift + j + 1
shift = 7
HERE IS A SIMPLE EXAMPLE
EXAMPLE
^
`shift = shift + 1`
`shift = 8`
HERE IS A SIMPLE EXAMPLE
EXAMPLE
^
`shift = shift + 1`
`shift = 9`
HERE IS A SIMPLE EXAMPLE
EXAMPLE
^
`j = 2; shift = shift + j + 1`
`shift = 12`
HERE IS A SIMPLE EXAMPLE
EXAMPLE
^
shift = shift + 1
shift = 13
HERE IS A SIMPLE EXAMPLE
EXAMPLE
^
shift = ?
Можно ли улучшить эвристику "сдвиг на 1" в данном случае?
</b>
В случае, если символа, встреченного в тексте, нет в паттерне, весь паттерн сдвигается правее этого символа.
А если есть? Предположим, что мы искали бы не весь паттерн, а только его часть - т.е. брали бы суффикс паттерна до встречи с символом A:
MPLE
тода решение было бы простым - сдвинуть паттерн правее символа. Иначе говоря, сдвигать его до тех пора, пока можно с уверенностью сказать, что "плохого" символа в паттерне нет.
Теперь вспомним, что у паттерна есть вторая часть (то есть просто его начало):
EXAMPLE
До этой части можно спокойно проматывать. Если мы сделаем так, то
окажется, что эвристика сдвигает последний (самый правый) символ A паттерна как раз до совпадения с таким же символом в тексте.
HERE IS A SIMPLE EXAMPLE
EXAMPLE
^
> (нам повезло, и поиск закончился, но это не всегда так)
Как расчитать сдвиг? Ведь "плохой" символ может встретиться не только в
конце паттерна, но и во время проверки "в середине" паттерна.
Препроцессинг должен покрывать все эти случаи.
# Подготовка символов
Нам потребуется функция `AlphabetIndex(char)`, которая переводит символ в
целое чисо: . При помощи этой функции будем получать фиксированные числовые индексы для символов из алфавита.
Вторая функция, более важная - это функция, которая подготовит таблицу для работы эвристики "плохого символа".
1 BadCharacterTable(pattern): 2 if Length(pattern) == 0: 3 return new Array[Int] of shape (0, |Alphabet|) 4 support = new Array[Int] of -1 of length |Alphabet| 5 table = new Array[Int] of shape (1 + Length(pattern), |Alphabet|) 6 for i in 0..Length(pattern) exclusively: 7 support[AlphabetIndex(pattern[i])] = i 8 for j in 0..Length(pattern) exclusively: 9 table[j][j+1] = support[j] 10 return table
Есть и более простой подход, который создает одномерную таблицу, в которой записаны для каждого символа алфавита расстояние от последнего символа до самого правого вхождения в строку. Если символа вообще нет, записывается длина строки:
1 BadCharacterVariant(pattern): 2 lp = Lentgh(pattern); 3 table = new Table of size |Alphabet| 4 for i in 0..|Alphabet| exclusively: 5 table[i] = lp; 6 for i in 0..(|lp| - 1) exclusively: 7 table[pattern[i]] = lp - 1 - i 8 return table
- но она несовместима с дальнейшей версией кода (из-за разных вовзращаемых значений)
Алфавит:
Паттерн:
Таблица:
G A T T A C A
A| -1, -1, 1, 1, 1, 4, 4, 6
C| -1, -1, -1, -1, -1, -1, 5, 5
G| -1, 0, 0, 0, 0, 0, 0, 0
T| -1, -1, -1, 2, 3, 3, 3, 3
Применяя подобную таблицу, можно гарантированно перемотать до "самого правого" относительно сравниваемого в данный момент "плохого" символа. Этот алгоритм является упрощением алгоритма Бойера-Мура и называется алгоритмом Бойера-Мура-Хорспула.
Чуть позже сравним его с "полной" версией.
1 BoyerMooreHorspool(text, pattern): 2 lp = Length(pattern) 3 lt = Length(text) 4 matches = new List 5 preprocessed = BadCharacterTable(pattern) 6 7 shift = 0 8 while shift < lt - lp + 1: 9 j = lp - 1 10 while j >= 0 and text[shift+j] == pattern[j]: 11 j -= 1 12 if j <= 0: 13 matches.append(shift) 14 shift += 1 15 else: 16 shift += (j - prepocessed[alphabet_index(text[shift+j])][j]) 17 // Альтернативный вариант с "одномерной" таблицей 18 // shift += prepocessed[alphabet_index(text[shift+j])]) - j 19 return matches
То, что мы только что рассмотрели выше, называется "эвристикой плохого символа".
Алгоритм Бойера-Мура использует дополнительную эвристику, называемую эвристикой "хорошего суффикса". При каждом несовпадении этот алгоритм вычисляет обе эвристики, и затем выбирает сдвиг по наилучшей (то есть мсаксимальный сдвиг).
Для начала - как эвристика вообще работает.
Пусть для данного выравнивания паттерна P и текста T подстрока t текста T совпадает с суффиксом P, но левее t происходит несовпадение.
SAGGTAGACTAAGACTTACCCAT
GAAGCAAG
t = AAG
Тогда, если существует, найдем самое правое вхождение t' == t в паттерн P, такое, что символ левее P отличается от символа левее t в паттерне P.
SAGGTAGACTAAGACTTACCCAT
GAAGCAAG
t = AAG после C
t' = AAG после G
Сдвинем паттерн P вправо так, чтобы t' из паттерна совпадало с t из текста.
SAGGTAGACTAAGACTTACCCAT
GAAGCAAG
Если же t' в паттерне P не существует, до сдвинем левый край P левее t из текста T как можно меньше, чтобы префикс P совпадал с суффиксом t, если такой префикс существует.
паттерн изменен на AGAGCAAG с целью демонстрации
SAGGTAGACTAAGACTTACCCAT
|AGAGCAAG
|||
|||
AAGAGCAAG
Если префикса нет, сдвинем паттерн левее подстроки t из текста.
SAGGTAGACTAAGACTTACCCAT
TCAGCAAG
Как работает эвристика "хорошего суффикса". Синим обозначен суффикс, размещенный в начале паттерна, в том числе его "воображаемая" часть.
HERE IS A SIMPLE EXAMPLE -> HERE IS A SIMPLE EXAMPLE
MPLEXAMPLE -> MPLEXAMPLE
^
Для сравнения, "плохой символ" в той же ситуации дает меньший сдвиг:
HERE IS A SIMPLE EXAMPLE -> HERE IS A SIMPLE EXAMPLE
IEXAMPLE -> IEXAMPLE
В алгоритме Бойера-Мура после вычисления выбирается лучшая эвристика
Для того, чтобы воспользвоаться перечисленными выше правилами, нам нужно составить таблицы сдвигов для того, чтобы усрорить процесс сдвига. Таблицы составляются за счет предобработки только паттерна.
Расчет таблиц для эвристики - сложный процесс, поэтому мы разберем его по частям, "снизу вверх".
Первая функция, которая потребуется, ищет повторения строки в самой себе
по заданным индексам, и возвращает длину совпавшей подстроки:
1 MatchLength(string, i, j): 2 if i == j: 3 return Length(string) - i 4 match_length = 0 5 ls = Length(s) 6 while i < ls and j < ls and string[i] == string[j] 7 match_length += 1 8 i += 1 9 j += 1 10 return match_length
Если вернуться к примеру выше, эта функция дала бы примерно такие результаты:
MatchLength(GAAGCAAG, 1, 5) == 3
Т.е. следующие три символа после 1-го и 5-го включительно совпадают:
GAAGCAAG
Для работы алгоритма используются две одномерные таблицы: L и F. Таблица F используется только в случае, если L[i] содержит "пустое" значение, или найдено совпадение паттерна и подстроки.
В таблице L содержится следующая информация:
Для всех i, L[i] содержит наибольшую (самую правую) позицию меньше n, такую, что строка P[i..n] совпадает с суффиксом P[1..L[i]], и символ, предшествующий этому суффиксу, не равен P[i-1] (то есть символу до P). Если такой позиции нет, L[i] равен 0.
Таблица F описывает величину наибольшего суффикса P[i..n] для F[i], который также является префиксом P. если такого суффикса нет, P[i] = 0
Пока становится только запутанее. Давайте посмотрим на примеры
Функция FindPrefixes(string) возвращает массив Z такой, что Z[i] содержит число, равное длине подстроки S[i], котороая при этом является и префиксом S. Если такого префикса нет, то Z[i] будет 0.
Пример:
FindPrefixes("coconut")
--
[7, 0, 2, 0, 0, 0, 0]
FindPrefixes("aaaaaaa")
--
[7, 6, 5, 4, 3, 2, 1]
Домашнее задание - реализовать данную функцию.
Как вы уже могли преположить, данная функция используется для создания таблиц L и H.
Однако, поскольку мы ведем сравнение "с конца" паттерна, то и функцию будем применять к развернутой строке (или сама функция пишется так, чтобы искать суффиксы - в принципе, разницы нет)
FindPrefixes(Reversed("EXAMPLE"))
--
[7, 0, 0, 0, 0, 0, 1]
FindPrefixes("ELMAXE")
--
[7, 0, 0, 0, 0, 0, 1]
По умолчанию используется эта таблица. Что она содержит?
При помощи этой таблицы можно вычислить сдвиг, если не совпал i-1 элемент, по следующей формуле:
shift = Length(pattern) - L[i] - 1
GoodSuffixTable("EXAMPLE")
--
E X A M P L E
[-1, -1, -1, -1, -1, -1, 0]
-- Сдвиг
[ -, -, -, -, -, -, -, 7]
- здесь единственный совпадающий суффикс - это последняя 'E'
GoodSuffixTable("GAAGCAAG")
--
G A A G C A A G
[-1, -1, -1, -1, -1, 3, -1, 0]
-- Сдвиг
[ -, -, -, -, -, 4, -, 7]
"G" в центре паттерна игнорируется, так как сдвигаться до суффикса 'G' перед которым идет 'A', когда алгоритм "споткнулся" об "AG", бессмысленно. Получается, что "сдвиг" нужен до первой 'G'.
GoodSuffixTable("GATGCAAG")
--
G A T G C A A G
[-1, -1, -1, -1, -1, -1, -1, 3]
-- Сдвиг
[ -, -, -, -, -, -, -, 4]
А теперь все нормально, потому что в центре перед 'G' расположено 'T', а не 'A'.
Таблица используется для того, чтобы правильно свидгать паттерн относительно текста так, чтобы на совпавший в прошлый раз суффикс накладывалась самая правая часть, содержащая ту же подстроку:
SAGGTAGACTAAGACTTACCCAT
GAAGCAAG
После сдвига:
SAGGTAGACTAAGACTTACCCAT
GAAGCAAG
Совпадение строки с самой собой не важно, поэтому L[0] = -1. Если L[i] сожержит -1 (подходящего суффикса нет), осуществляется переход к следуюущей таблице, "таблице полного сдивга" H ("full shift table"). В случае, если произошло несовпадение символов текста и паттерна на i-1-м символе, выполняется сдвиг на Length(pattern) - L[i].
L[i] = k, где S[i:] совпадает с S[:k]. Несовпадающие суффиксы равны -1. Например, для EXAMPLE будет:
Псевдокод алгоритма:
GoodSuffixTable(string):
L = new Array[Int] of -1 of size Length(string)
Z = FindPrefixes(Reversed(S))
Z = Reverse(Z)
for j in 0..Length(S)-1 exclusively:
i = Length(S) - Z[j]
if i != Length(S):
L[i] = j
return L
Это таблица частично совпадающих префиксов. Переходим к ней в ситуации, если совпадающего суффикса не существует.
Используется схожим с таблицей L образом: если i-1-й элемент паттерна не совпал, то паттерн перемещается на Length(P) - F[i] вправо.
По сути, это таблица, содержащую размеры следующей вещи: самого длинного суффикса строки S[i:] (можно сказать, что это суффикс суффикса паттерна), который одновременно является и префиксом S.
На примере "EXAMPLE":
FullShiftTable("BONOBO")
--
'O'
'B O'
'O B O'
'N O B O'
'O N O B O'
'N O N O B O'
--
[6, 2, 2, 2, 2, 0]
-- Сдвиг
[0, 4, 4, 4, 4, 6]
- Другой пример:
FullShiftTable("EXAMPLE")
--
[7, 1, 1, 1, 1, 1, 1]
-- Сдвиг
[0, 6, 6, 6, 6, 6, 6]
Псевдокод функции
FullShiftTable(string):
F = new Array[Int] of 0 of size Length(string)
Z = FindPrefixes(string)
Z = Reversed(Z)
longest = 0
for i in Length(Z):
if Z[i] == i+1:
longest = max(Z[i], longest)
else:
longest
F[-i-1] = longest
return F
Теперь мы (надеюсь) готовы к тому, чтобы добавить эвристику "хорошего суффикса" к основному алгоритму.
Добавим новую эвристику в алгоритм Бойера-Мура-Хорспула:
BoyerMoore(pattern, text):
lp = Length(pattern)
lt = length(text)
if lp == 0 or lt == 0 or lt < lp:
return []
matches = new List
bad_character = BadCharacterTable(pattern)
L = GoodSuffixTable(pattern)
F = FullShiftTable(pattern)
previous_k = -1
shift = 0
while i >= 0 and shift + i > previous_k and pattern[i] = text[shift+i]:
i -= 1
if i == -1 or shift + i == previous_k:
matches.append(shift)
else:
char_shift = i - bad_character[AlphabetIndex(text[shift + i])][i]
if i + 1 == lp:
suffix_shift = 1
else if L[i+1] == -1:
suffix_shift = lp - F[i+1]
else:
suffix_shift = lp - L[i+1] - 1
max_shift = Max(char_shift, suffix_shift)
if max_shift == i+1:
previous_k = matches[-1] if matches is not empty else -1
shift += max_shift
return matches
</font>
Реализовать функцию FindPrefixes, которая создает таблицу префиксов (примеры работы есть в материале):
Функция FindPrefixes(string) возвращает массив Z такой, что Z[i] содержит число, равное длине подстроки S[i], котороая при этом является и префиксом S. Если такого префикса нет, то Z[i] будет 0.
Есть тест-кейсы: файл preprocess_test_cases.txt.
Традиционно, первая строка - количество записей, первый столбец -
строка, далее через пробел - записанные последовательно значения из
таблицы префиксов для данной строки.
string_matching_test_cases.tsv.
Это .tsv без заголовка, но в первой строчке общее число записей. Первый
столбец - текст (содержит пробелы), после первого таба - столбец с
паттерном (также может содержать пробелы), в последнем столбце через
пробел все вхождения паттерна в текст, в том числе, перекрывающиеся частично. Если вхождений нет, столбец пуст.</font>