Суффиксный массив — лексикографически отсортированный массив всех суффиксов строки. Эта структура данных была разработана Юджином Майерсом и Уди Манбером как более экономная альтернатива суффиксному дереву с точки зрения необходимой памяти. Она часто применяется там, где необходим быстрый поиск подстрок, например в преобразовании Барроуза — Уилера (BWT), а также в качестве структуры данных в поисковом индексе.
Пример
Рассмотрим строку «abracadabra» длиной 11 символов.
a b r a c a d a b r a
1 2 3 4 5 6 7 8 9 10 11
Отсортированный список её суффиксов:
a
abra
abracadabra
acadabra
adabra
bra
bracadabra
cadabra
dabra
ra
racadabra
Суффиксный массив этой строки — {11,8,1,4,6,9,2,5,7,10,3}, потому что суффикс «a» начинается с 11-го знака, суффикс «abra» — с 8-го, и так далее, вплоть до последнего суффикса «racadabra», который начинается с третьего символа исходного слова.
Теперь с помощью этого массива можно легко найти все подстроки. Например, если нужно найти подстроку «ab», достаточно найти все суффиксы, которые начинаются на «ab». За счёт сортировки по алфавиту они находятся рядом друг с другом. Используя бинарный поиск, мы находим 2-й и 3-й суффиксы «abra» и «abracadabra», которым соответствуют 2-й и 3-й элемент суффиксного массива (8 и 1). Это означает, что искомая подстрока «ab» встречается на первом и восьмом символе в исходном слове.
Построение
Суффиксный массив можно построить как с помощью суффиксного дерева, так и без него, дополнив строку до циклической длины степени двойки, и применив к нему конкретный алгоритм.
Через суффиксное дерево
- Строим суффиксное дерево для строки T$. Где T — текст.
- В этом суффиксном дереве запускаем поиск в глубину с приоритетом выбора лексиграфически минимальных рёбер.
- Во время поиска считаем, что $ (сентинел) лексикографически наименьший символ.
- Приход в лист достижение некоторого ещё не рассмотренного в данный момент лексикографически наименьшего суффикса, значение в листе которого, начальный индекс в, нужно записать в текущую ячейку суффиксного массива.
- Так получается суффиксный массив для всего текста.
|
Сложность построения — [math]\displaystyle{ O(|T|) }[/math], линия включает в себя построение суффиксного дерева и обход в глубину.
Поиск
Поиск в суффиксном массиве можно осуществить через бинарный поиск. Его худшая оценка [math]\displaystyle{ O(n\log{m}) }[/math]. Но можно ускорить до [math]\displaystyle{ O(n + \log_2{m}) }[/math].
Наивный бинарный поиск
- Идея поиска в том, что если образец встречается в тексте, то все суффиксы, начинающиеся с [math]\displaystyle{ P }[/math] в суффиксном массиве [math]\displaystyle{ Pos }[/math] будет располагаться рядом.
- Запускаем двоичный поиск [math]\displaystyle{ P }[/math] в суффиксном массиве [math]\displaystyle{ Pos }[/math] и находим такой наименьший индекс [math]\displaystyle{ i }[/math]: [math]\displaystyle{ Pos(i-1) }[/math] не начинается с [math]\displaystyle{ P }[/math] и наибольший индекс [math]\displaystyle{ i' }[/math]: [math]\displaystyle{ Pos(i'+1) }[/math] тоже не начинается с [math]\displaystyle{ P }[/math].
- Тогда образец входит в позициях [math]\displaystyle{ Pos(i) }[/math] до [math]\displaystyle{ Pos(i') }[/math].
- Если существует много префиксов паттерна, то оценка работы падает до [math]\displaystyle{ O(n\log{m}) }[/math].
|
Простое ускорение
- [math]\displaystyle{ L }[/math], [math]\displaystyle{ R }[/math] — границы интервала поиска. На начале [math]\displaystyle{ L = 1 }[/math], [math]\displaystyle{ R = m }[/math].
- Запоминаем длину префиксов [math]\displaystyle{ Pos(L) }[/math], [math]\displaystyle{ Pos(R) }[/math], совпадающих с префиксом [math]\displaystyle{ P: l, r }[/math].
- [math]\displaystyle{ mlr = min(l,r) }[/math].
- При очередном сравнении в позиции [math]\displaystyle{ M = \frac{L + R}{2} }[/math] начинаем обрабатывать символы не с первой позиции, а с [math]\displaystyle{ mlr(l,r) + 1 }[/math].
- Обычно время работы [math]\displaystyle{ O(n + \log{m}) }[/math], но худшее время работы всё ещё [math]\displaystyle{ O(n\log{m}) }[/math].
|
Ускорение через LCP
Наибольший общий префикс (Шаблон:Lang-en) — для двух строк [math]\displaystyle{ S_1 }[/math], [math]\displaystyle{ S_2 }[/math] [math]\displaystyle{ LCP(S_1, S_2) }[/math] — длина наибольшего совпадающего префикса.
В этом алгоритме будем считать, что [math]\displaystyle{ LCP }[/math] для любых двух суффиксов вычисляется за [math]\displaystyle{ O(1) }[/math]. Функция вычисляется на этапе препроцессинга при построении дерева. Также верно утверждение: [math]\displaystyle{ LCP(i, j) = min(LCP(k, k + 1)), i\leq k \lt j }[/math].
Благодаря этой функции можно оптимизировать бинарный поиск по суффиксному массиву.
Лемма: Если на левой и правой границе ([math]\displaystyle{ L }[/math], [math]\displaystyle{ R }[/math] соответственно индексы суффиксного массива) совпадают первые [math]\displaystyle{ k }[/math] символов суффикса, то столько же символов будет совпадать для всех суффиксов на отрезке [math]\displaystyle{ [L,R] }[/math].
- [math]\displaystyle{ L = 1 }[/math], [math]\displaystyle{ R = |T| }[/math], [math]\displaystyle{ l = LCP(P, L) }[/math], [math]\displaystyle{ r = LCP(P, R) }[/math]. Дальше возможны следующие случаи
- [math]\displaystyle{ l = r }[/math].
- Сравниваем суффикс в [math]\displaystyle{ M = \frac{L + R}{2} }[/math] с паттерном с позиции [math]\displaystyle{ l + 1 }[/math].
- Суффикс лексикографически больше или равен [math]\displaystyle{ P }[/math] и на [math]\displaystyle{ i }[/math] позиции в суффиксе произошло несовпадение (если произошло лексикографическое совпадение [math]\displaystyle{ M }[/math] и [math]\displaystyle{ P }[/math], то считаем [math]\displaystyle{ i }[/math] равным [math]\displaystyle{ |P|+1 }[/math]), то меняем границы поиска: [math]\displaystyle{ L = M, R = R, l = i - 1 }[/math].
- Иначе меняем границы так: [math]\displaystyle{ L = L, R = M, r = i - 1 }[/math].
- [math]\displaystyle{ l \gt r }[/math]. Проверяем [math]\displaystyle{ LCP(L, M), M = \frac{L+R}{2} }[/math].
- [math]\displaystyle{ LCP(L, M) \gt l }[/math]. В таком случае после позиции [math]\displaystyle{ l }[/math] в суффиксе на позиции [math]\displaystyle{ M }[/math] следует некоторое количество тех же самых символов, что и в [math]\displaystyle{ L }[/math], которые не совпадают с паттерном (если бы совпадали, то [math]\displaystyle{ l }[/math] было бы больше). Значит, нужно изменить границы следующим образом: [math]\displaystyle{ L = M,R = R, l = l }[/math].
- [math]\displaystyle{ LCP(L, M) \lt l }[/math], это значит, что после позиции [math]\displaystyle{ LCP(L, M) }[/math] в суффиксе на позиции [math]\displaystyle{ M }[/math] следует несовпадение с некоторыми символами префикса [math]\displaystyle{ L }[/math], причём в [math]\displaystyle{ L }[/math] содержится большая часть совпадения с паттерном — значит в отрезке [math]\displaystyle{ [M, R] }[/math] точно не найдется вхождений паттерна. Изменить границы нужно следующим образом: [math]\displaystyle{ L = L, R = M, r = LCP(L, M) }[/math].
- [math]\displaystyle{ LCP(L,M)=l }[/math], это значит, что на отрезке [math]\displaystyle{ [L,M] }[/math] во всех суффиксах совпадают первые [math]\displaystyle{ l }[/math] символов, и нельзя сразу сказать, в какой подотрезок нужно пойти. Для разрешения этого необходимо сравнить с паттерном [math]\displaystyle{ P }[/math] следующие за позицией [math]\displaystyle{ l }[/math] символы в суффиксе [math]\displaystyle{ M }[/math]. Если [math]\displaystyle{ M }[/math] лексикографически меньше или равно [math]\displaystyle{ P }[/math] и на [math]\displaystyle{ i }[/math]-ой позиции произошло несовпадение (если произошло лексикографическое совпадение [math]\displaystyle{ M }[/math] и [math]\displaystyle{ P }[/math], то считаем [math]\displaystyle{ i }[/math] равным [math]\displaystyle{ |P|+1 }[/math]), то изменяем границы так: [math]\displaystyle{ L=M }[/math], [math]\displaystyle{ R=R }[/math], [math]\displaystyle{ l = i - 1 }[/math]; иначе ([math]\displaystyle{ M }[/math] лексикографически больше): [math]\displaystyle{ R=M }[/math], [math]\displaystyle{ L=L }[/math],[math]\displaystyle{ r = i - 1 }[/math].
- [math]\displaystyle{ l \lt r }[/math]. Проверяем [math]\displaystyle{ LCP(R, M), M = \frac{L+R}{2} }[/math] и сравниваем с [math]\displaystyle{ r }[/math] как на прошлом шаге, но [math]\displaystyle{ L }[/math] меняем на [math]\displaystyle{ R }[/math] и [math]\displaystyle{ l }[/math] на [math]\displaystyle{ r }[/math].
- Алгоритм работает, пока [math]\displaystyle{ l }[/math] и [math]\displaystyle{ r }[/math] не станут равными [math]\displaystyle{ |P| }[/math]. Это значит, что есть отрезок совпадения. Если не выполняется инвариант [math]\displaystyle{ L \lt P \lt R }[/math], то паттерна как подстроки в тексте нет.
|
Такое сверхускорение даёт время [math]\displaystyle{ O(|P|+\log_2{|T|}) }[/math], так как выполняется [math]\displaystyle{ \log_2{|T|} }[/math] итераций по суффиксному массиву.
Связанные алгоритмы
См. также
Ссылки
Литература
Шаблон:Строки