Алгоритм поиска подстроки в строке с помощью суффиксного массива — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
м (rollbackEdits.php mass rollback)
 
(не показано 190 промежуточных версий 10 участников)
Строка 1: Строка 1:
Рассмотрим такую задачу: у нас есть образец <tex> p </tex>, строка <tex> s </tex>, [[суффиксный массив|суффиксный массив]] <tex> array </tex>, построенный для строки <tex> s </tex>. Необходимо найти все вхождения образца <tex> p </tex> в строку <tex> s </tex>.
+
Далее будут рассмотрены некоторые способы нахождения всех вхождений образца в текст с помощью [[суффиксный массив|суффиксного массива]].
  
Для наглядности рассмотрим такой пример: образец <tex> iss </tex>, строка <tex> mississippi </tex>. <br>
+
== Наивный алгоритм поиска ==
Вот суффиксный массив для данной строки:
 
  
{| border="1"
+
Простейший способ узнать, встречается ли образец в тексте, используя суффиксный массив, {{---}} взять первый символ образца и [[Целочисленный двоичный поиск|бинарным поиском]] по [[суффиксный массив|суффиксному массиву]] найти диапазон с суффиксами, начинающимися на такую же букву. Так как все элементы в полученном диапазоне отсортированы, а первые символы одинаковые, то оставшиеся после отбрасывания первого символа суффиксы тоже отсортированы. А значит, можно повторять процедуру сужения диапазона поиска уже по второму, затем третьему и так далее символу образца до получения либо пустого диапазона, либо успешного нахождения всех символов образца.
|width="20"|#
 
|width="150"|суффикс
 
|width="100"|номер суффикса
 
|-
 
|1
 
|i
 
|11
 
|-
 
|2
 
|ippi
 
|8
 
|-
 
|3
 
|issippi
 
|5
 
|-
 
|4
 
|ississippi
 
|2
 
|-
 
|5
 
|mississippi
 
|1
 
|-
 
|6
 
|pi
 
|10
 
|-
 
|7
 
|ppi
 
|9
 
|-
 
|8
 
|sippi
 
|7
 
|-
 
|9
 
|sissippi
 
|4
 
|-
 
|10
 
|ssippi
 
|6
 
|-
 
|11
 
|ssissippi
 
|3
 
|}
 
  
== Способы поиска ==
+
Бинарный поиск работает за время равное <tex> O(\log|s|) </tex>, а сравнение суффикса с образцом не может превышать длины образца.
  
=== Простейший поиск подстроки ===
+
Таким образом время работы алгоритмы <tex> O(|p|\log|s|)</tex>, где <tex> s </tex> {{---}} текст, <tex> p </tex> {{---}} образец.
  
Простейший способ узнать, встречается ли образец в тексте, используя суффиксный массив, это взять первый символ образца и бинарным поиском по суффиксному массиву (массив у нас отсортирован) найти диапазон с суффиксами, начинающимися на такую же букву. Так как все элементы в полученном диапазоне отсортированы, а первые символы одинаковые, то оставшиеся после отбрасывания первого символа суффиксы тоже отсортированы. А значит, можно повторять процедуру сужения диапазона поиска уже по второму, затем третьему и так далее символу образца до получения либо пустого диапазона, либо успешного нахождения всех символов образца. Бинарный поиск работает за <tex> O(log|s|) </tex>, а сравнение суффикса с образцом не может превышать длины образца. Таким образом время работы алгоритмы <tex> O(|p|log|s|)</tex>. <br>
+
=== Псевдокод ===
В примере поиск будет выглядеть так:
+
 
 +
'''Поиск диапазона '''
 +
 
 +
<tex> \mathtt {cmp (k)}</tex> {{---}}  функция, сравнивающая строки по <tex>k</tex>-тому символу.
 +
 
 +
<tex> \mathtt {lower}</tex>_<tex>\mathtt {bound (left, right, value, cmp)}</tex>, <tex> \mathtt {upper}</tex>_<tex>\mathtt {bound (left, right, value, cmp)}</tex> {{---}} функции бинарного поиска.
 +
 
 +
Элементы строк нумеруются с единицы
 +
 +
'''function''' elementary_search(p: '''String''', s: '''String'''):
 +
    left = 0                                        <font color=darkgreen> // left, right {{---}} границы диапазона </font>
 +
    right = n                                        <font color=darkgreen> //  n {{---}}  длина образца </font>
 +
    '''for''' i = 1 '''to''' n
 +
        left = lower_bound(left, right, p[i], cmp (i) )
 +
        right = upper_bound(left, right, p[i], cmp (i) )
 +
    '''if''' (right - left > 0) 
 +
        print left                 
 +
        print right               
 +
    '''else'''
 +
        print "No matches"
 +
 
 +
== Более быстрый поиск ==
 +
 
 +
Существует более быстрый алгоритм поиска образца в строке. Для этого используется <tex>\mathtt {lcp} </tex> ([[Суффиксный массив#Применения|longest common prefix]]).
 +
 
 +
=== Условные обозначения ===
 +
 
 +
* <tex> \mathtt{answer} </tex>_<tex>\mathtt{left}</tex> и <tex>\mathtt{answer} </tex>_<tex>\mathtt{right}</tex> {{---}} левая и правая границы диапазона ответов в суффиксном массиве <tex> array </tex>,
 +
* <tex> L </tex> {{---}} левая граница текущего диапазона поиска (изначально равна <tex>0</tex>),
 +
* <tex> R </tex> {{---}} правая граница текущего диапазона поиска (изначально равна <tex> |S| - 1 </tex>),
 +
* <tex> M = (L + R) / 2 </tex> {{---}} середина текущего диапазона поиска,
 +
* <tex> l = </tex> <tex>\mathtt {lcp(array[L], p)} </tex> {{---}} длина общего префикса образца и левого края текущего диапазона поиска,
 +
* <tex> r = </tex> <tex>\mathtt {lcp(array[R], p)} </tex> {{---}} длина общего префикса образца и правого края текущего диапазона поиска,
 +
* <tex> m_l = </tex> <tex>\mathtt {lcp(array[L], array[M])} </tex> {{---}} длина общего префикса середины текущего диапазона и левого края текущего диапазона поиска,
 +
* <tex> m_r = </tex> <tex>\mathtt {lcp(array[M], array[R])} </tex> {{---}} длина общего префикса середины текущего диапазона и правого края текущего диапазона поиска.
 +
 
 +
=== Алгоритм ===
 +
 
 +
Если диапазон ответов не пустой, то у любого суффикса в пределах диапазона ответов есть префикс, который полностью совпадает с образцом.
 +
 +
В самом начале просто посчитаем <tex> l</tex> и <tex> r </tex> за линейное время с помощью [[Алгоритм Касаи и др.|алгоритма Касаи, Арикавы, Аримуры, Ли и Парка]], а во время выполнения алгоритма прямой пересчет производиться не будет, изменения будут происходить за <tex> O(1) </tex>.
 +
 
 +
Подсчет <tex> m_l </tex> и <tex> m_r </tex> можно производить за <tex> O(1) </tex>, если применять [[Алгоритм Фарака-Колтона и Бендера|алгоритм Фарака-Колтона и Бендера]]. Любая пара суффиксов <tex> array </tex> из диапазона <tex> [L, M] </tex> имеет хотя бы <tex> m_l </tex> совпадений в префиксах. Аналогично любая пара суффиксов <tex> array </tex> из диапазона <tex> [M, R] </tex> имеет хотя бы <tex> m_r </tex> совпадений в префиксах.
 +
 
 +
=== Поиск границ диапазона ответов ===
 +
 
 +
Рассмотрим поиск левой границы диапазона ответов <tex>\mathtt{answer} </tex>_<tex>\mathtt{left}</tex>.
 +
 
 +
Сразу проверим образец с суффиксами по краям исходного диапазона поиска <tex> L </tex> и <tex> R </tex>: если образец лексикографически больше последнего суффикса <tex> array </tex> или меньше первого суффикса, то образец не встречается в строке вовсе и поиск можно прекратить.
 +
 
 +
<tex> \mathtt{answer} </tex>_<tex>\mathtt{left}</tex> ищется при помощи бинарного поиска по суффиксному массиву <tex> array </tex>. На каждом шаге поиска нам надо определять, на каком отрезке <tex> [L, M] </tex> или <tex> [M, R] </tex> надо продолжать поиск границы <tex> \mathtt{answer} </tex>_<tex>\mathtt{left}</tex> . Каждую итерацию бинарного поиска будем сравнивать <tex> l </tex> и <tex> r </tex>. Если <tex> l \geqslant r </tex>, то возможно одно из трех:
 +
 
 +
# <tex> m_l > l  </tex>. Это означает, что каждая пара суффиксов из диапазона <tex> [L, M] </tex> имеет между собой больше совпадений, чем суффикс с левого края с образцом, поэтому продолжим поиск в диапазоне <tex> [M, R] </tex>. Значение <tex> l </tex> при этом не меняется, а <tex> L = M </tex>.
 +
# <tex> m_l = l </tex>. Это означает, что у каждого суффикса из <tex> [L, M] </tex> есть хотя бы <tex> l </tex> совпадений с образцом. Проверим суффикс в позиции <tex> M </tex>, так как с ним совпадений у образца может получиться больше. Начнем сравнивать суффикс в позиции <tex> M </tex> начиная с <tex> l </tex>-ого символа. Мы либо найдем полное вхождение образца в суффикс, либо на каком-то шаге <tex> k </tex> получим несоответствие. В первом случае <tex> R = M </tex> и <tex> r = |p| </tex>, так как мы ищем левую границу диапазона ответов. Во втором случае все зависит от лексикографического несовпадения. Если символ <tex> l + k + 1 </tex> у образца меньше, чем у суффикса, то <tex> R = M </tex> и <tex> r = l + k + 1</tex>, иначе <tex> L = M </tex> и <tex> l = l + k + 1</tex>.
 +
# <tex> m_l < l </tex>. Это означает, что совпадений у суффикса с левого края диапазона поиска с образцом больше, чем у суффикса в позиции <tex> M </tex>. Очевидно, что поиск надо продолжать между <tex> L </tex> и <tex> M </tex>, то есть <tex> R = M </tex>, а новое значение <tex> r = m_l </tex>.
 +
Если <tex> l < r </tex>, то действия аналогичны. Также три случая:
 +
# <tex> m_r > r </tex>. Сдвигаем <tex> R </tex> в <tex> M </tex>. Значение <tex> r </tex> не изменяется.
 +
# <tex> m_r = r </tex>. Считаем <tex>\mathtt {lcp} </tex> для образца и суффикса, стоящего в позиции <tex> M </tex>, начиная с позиции <tex> r </tex>.
 +
# <tex> m_r < r </tex>. Сдвигаем <tex> L </tex> в <tex> M </tex>, <tex> l = m_r </tex>.
 +
Бинарный поиск будет работать до тех пор, пока <tex> R - L > 1 </tex>. После этого можно присвоить левой границе диапазона ответов <tex> \mathtt{answer} </tex>_<tex>\mathtt{left} = R </tex> и переходить к поиску правой границы диапазона ответов <tex> \mathtt{answer} </tex>_<tex>\mathtt{right}</tex> .
 +
 
 +
Рассуждения при поиске <tex> \mathtt{answer} </tex>_<tex>\mathtt{right}</tex>  аналогичны, только нужно не забыть изменить границы поиска на изначальные <tex> L = 0 </tex> и <tex> R = |s| - 1 </tex>.
 +
 
 +
Таким образом часть бинарного поиска мы сделаем при сравнении нескольких <tex>\mathtt {lcp} </tex> между собой(каждое за <tex> O(1) </tex>), а если дойдет до сравнения символов, то любой символ <tex> p </tex> сравнивается не более одного раза(при сравнении мы берем <tex>\mathtt {max}</tex><tex>(l, r) </tex>, а значит никогда не возвращаемся назад). В самом начале мы посчитали <tex> l </tex> и <tex> r </tex> за <tex> O(p) </tex>. В итоге получаем сложность алгоритма <tex> O(p + log(s)) </tex>. Правда нужен предподсчет, чтобы можно было брать <tex>\mathtt {lcp} </tex> для двух любых суффиксов <tex> array </tex> за <tex> O(1) </tex>, начиная с позиции <tex> r </tex>.
 +
 
 +
===Рисунки===
 +
 
 +
Черная вертикальная линия на рисунке обозначает <tex>\mathtt {lcp} </tex> от <tex> i </tex>-го суффикса суффиксного массива <tex> array </tex> и образца <tex> p </tex>. Чем линия длиннее, тем совпадений символов больше.
 +
 
 +
<tex> L </tex>, <tex> M </tex> и <tex> R </tex> {{---}} то же самое, что в алгоритме. Кроме того, самая левая черная вертикальная линия на каждом рисунке означает <tex> l </tex>, аналогично, самая правая черная вертикальная линия на каждом рисунке означает <tex> r</tex>.
 +
 
 +
Переменная <tex> m_l </tex> {{---}} это <tex>\mathtt {lcp} </tex> в суффиксном массиве на промежутке <tex> [L, M] </tex>. Переменная <tex> m_r </tex> {{---}} это <tex>\mathtt {lcp} </tex> в суффиксном массиве на промежутке <tex> [M, R] </tex>.
 +
Серым цветом выделен <tex>\mathtt {lcp} </tex> в суффиксном массиве на рассматриваемом промежутке.
 +
 
 +
Иллюстраци возможных случаев при <tex> l \geqslant r </tex>:
 +
 
 +
[[Файл:left.png]]
 +
 
 +
Иллюстрации возможных случаев при <tex> l < r </tex>:
 +
 
 +
[[Файл:Right2.png]]
 +
 
 +
===Псевдокод===
 +
Массивы и строки нумеруются с нуля.
 +
 
 +
Сравнения <tex><_z  ,  >_z  ,  =_z  , \leqslant_z  , \geqslant_z </tex> означают лексикографическое сравнение двух строк по их первым <tex>z</tex> символам.
 +
 
 +
Сравнения <tex>< , > , == ,  \leqslant ,  \geqslant </tex> при применении к строкам означают полное лексикографическое сравнение строк.
 +
 
 +
Функция <tex>\mathtt {common(z,s, p)}</tex> ищет количество совпадений символов строк <tex>s</tex> и <tex>p</tex> начиная с позиции <tex>z</tex>.
  
{| border="1"
+
<tex>n</tex> {{---}} длина строки <tex>s</tex>, <tex>w</tex> {{---}} длина строки <tex>p</tex>.
|width="80"|образец
 
|width="150"|'''''i'''ss''
 
|width="150"|'''''is'''s''
 
|width="150"|'''''iss'''''
 
|-
 
|
 
|'''''i'''''
 
|i
 
|i
 
|-
 
|
 
|'''''i'''ppi''
 
|ippi
 
|ippi
 
|-
 
|
 
|'''''i'''ssippi''
 
|'''''is'''sippi''
 
|'''''iss'''ippi''
 
|-
 
|
 
|'''''i'''ssissippi''
 
|'''''is'''sissippi''
 
|'''''iss'''issippi''
 
|-
 
|
 
|mississippi
 
|mississippi
 
|mississippi
 
|-
 
|
 
|pi
 
|pi
 
|pi
 
|-
 
|
 
|ppi
 
|ppi
 
|ppi
 
|-
 
|
 
|sippi
 
|sippi
 
|sippi
 
|-
 
|
 
|sissippi
 
|sissippi
 
|sissippi
 
|-
 
|
 
|ssippi
 
|ssippi
 
|ssippi
 
|-
 
|
 
|ssissippi
 
|ssissippi
 
|ssissippi
 
|}
 
Как видно из примера образцу удовлетворяют суффиксы 3 и 4, начинающиеся на 5 и 2 позициях в строке соответственно.
 
  
=== Псевдокод ===
+
В алгоритме используются переменные, введенные выше в разделе "более быстрый поиск".
 +
  
Поиск диапазона
+
Поиск левой границы ответов <tex> answer </tex>_<tex>left</tex>.
/*p - образец
 
n - длина образца
 
left - левая граница диапазона // изначально равна единице
 
right - правая граница диапазона // изначально равна длине строки
 
lh - вспомогательная переменная для определения левой границы диапазона 
 
rg - вспомогательная переменная для определения правой границы диапазона
 
find - функция уточнения диапазона
 
элементы строк и массивов нумеруются с единицы*/
 
for i = 1 to n {
 
  lh = n + 1
 
  rh = 0
 
  find(left, right, i)
 
  left = lh
 
  right = rh
 
}
 
if (left != 0 && right != n + 1) { // если диапазон не пуст
 
  yield left // вывод левой границы диапазона
 
  yield right // вывод правой границы диапазона
 
} else
 
  yield "No matches" // вывод информации об отсутствии вхождений
 
  
Бинарный поиск для уточнения диапазона - функция find(l, r, k)
+
'''function''' find_answer_left(p: '''String''', s: '''String'''): '''int'''
/*l - левая граница диапазона при поиске
+
    l = '''lcp'''(p, s[array[0]])
r - правая граница диапазона при поиске
+
    r = '''lcp'''(p, s[array[n - 1]])
k - номер символа образца, с которым происходит проверка на данном шаге
+
    '''if''' (l == w or p < s[array[0]])
s - строка
+
        answer_left = 0
length - длина строки
+
    '''else''' '''if''' (p > s[array[n - 1])
array - суффиксный массив
+
        answer_left = n
x - индекс, стоящий по середине между l и r*/
+
    '''else'''
if (l > r)
+
        L = 0
  return
+
        R = n - 1
x = (l + r) / 2
+
        '''while''' (R - L > 1) '''do'''
if (array[x] + k - 1 <= length){
+
            M = (L + R) / 2
  if (s[array[x] + k - 1] == p[k]){
+
            m_l = '''lcp'''(array[L], array[M])
    if (x < lh)
+
            m_r = '''lcp'''(array[M], array[R])
      lh = x
+
            '''if''' (l <tex>\geqslant</tex> r)
    if (x > rh)
+
                '''if''' (m_l <tex>\geqslant</tex> l)
      rh = x
+
                    m = l + '''common'''(l, s[array[M]], p)
    find(l, x - 1, k)
+
                '''else'''
    find(x + 1, r, k)
+
                    m = m_l
  } else {
+
            '''else'''
  if (s[array[x] + k - 1] > p[k]) {
+
                '''if''' (m_r <tex>\geqslant</tex> r)
    find(l, x - 1, k)
+
                    m = r + '''common'''(r, s[array[M]], p)
  } else {
+
                '''else'''
  if (s[array[x] + k - 1] < p[k]) {
+
                    m = m_r
    find(x + 1, r, k)
+
            '''if''' (m == w || p <tex>\leqslant</tex><tex>_m</tex> s[array[M]]){
  }
+
                R = M
} else {
+
                r = m
  find(l, x - 1, k)
+
            '''else'''
  find(x + 1, r, k)
+
                L = M
}
+
                l = m
 +
        answer_left = R
  
=== Более быстрый поиск ===
+
== См. также ==
 +
* [[Алгоритм цифровой сортировки суффиксов циклической строки]]
 +
* [[Алгоритм Касаи и др.]]
 +
* [[Построение суффиксного массива с помощью стандартных методов сортировки]]
  
На самом деле нам не обязательно сравнивать всю искомую строку с элементом суффиксного массива. На каждой итерации бинарного поиска мы уточняем некий диапазон, внутри которого может находиться искомый элемент. Все строки в таком диапазоне в некотором смысле похожи. А именно, у данных строк может быть общий префикс с искомой строкой, так как у тех, что остались вне диапазона, общего префикса уж точно не будет (в том смысле, что мы не рассматриваем уже обработанную часть образца как часть префикса). <br>
+
==Источники информации==
 +
* [http://habrahabr.ru/blogs/algorithm/115346/ Habrahabr {{---}} Суффиксный массив {{---}} удобная замена суффиксного дерева]
 +
*U. Manber and G. Mayers. {{---}} "Suffix arrays: A new method for on-line string searches"
  
Допустим, мы знаем длину общего префикса оставшегося образца с краями текущего диапазона l = lcp(X,S[L]) и r = lcp(X,S[R]) для левого и правого края соответственно. Первое утверждение заключается в том, что для любой строки внутри диапазона lcp не меньше, чем минимум этих двух чисел. Если бы это было не так, то значит при неизменной начальной части префикса была бы позиция, где символ сначала совпадал бы с соответствующим символом образца, потом не совпадал, а потом снова совпадал. Это противоречило бы отсортированности диапазона. Важно хорошо проникнуться этой идеей, так как дальше мы ее будем использовать как нечто само собой разумеющееся. Второе утверждение очевидно: если общий префикс образца и любой строки внутри диапазона не меньше m=min(l,r), то m символов можно пропускать сразу, зная, что они совпадают в любом случае, и сравнивать только начиная с m+1 (и получая в результате lcp для данной строки).
+
[[Категория:Алгоритмы и структуры данных]]
 +
[[Категория:Структуры данных]]
 +
[[Категория:Суффиксный массив]]

Текущая версия на 19:22, 4 сентября 2022

Далее будут рассмотрены некоторые способы нахождения всех вхождений образца в текст с помощью суффиксного массива.

Наивный алгоритм поиска

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

Бинарный поиск работает за время равное [math] O(\log|s|) [/math], а сравнение суффикса с образцом не может превышать длины образца.

Таким образом время работы алгоритмы [math] O(|p|\log|s|)[/math], где [math] s [/math] — текст, [math] p [/math] — образец.

Псевдокод

Поиск диапазона

[math] \mathtt {cmp (k)}[/math] — функция, сравнивающая строки по [math]k[/math]-тому символу.

[math] \mathtt {lower}[/math]_[math]\mathtt {bound (left, right, value, cmp)}[/math], [math] \mathtt {upper}[/math]_[math]\mathtt {bound (left, right, value, cmp)}[/math] — функции бинарного поиска.

Элементы строк нумеруются с единицы

function elementary_search(p: String, s: String): 
    left = 0                                          // left, right — границы диапазона 
    right = n                                         //  n —  длина образца 
    for i = 1 to n 
        left = lower_bound(left, right, p[i], cmp (i) )
        right = upper_bound(left, right, p[i], cmp (i) )
    if (right - left > 0)   
        print left                   
        print right                 
    else
        print "No matches"

Более быстрый поиск

Существует более быстрый алгоритм поиска образца в строке. Для этого используется [math]\mathtt {lcp} [/math] (longest common prefix).

Условные обозначения

  • [math] \mathtt{answer} [/math]_[math]\mathtt{left}[/math] и [math]\mathtt{answer} [/math]_[math]\mathtt{right}[/math] — левая и правая границы диапазона ответов в суффиксном массиве [math] array [/math],
  • [math] L [/math] — левая граница текущего диапазона поиска (изначально равна [math]0[/math]),
  • [math] R [/math] — правая граница текущего диапазона поиска (изначально равна [math] |S| - 1 [/math]),
  • [math] M = (L + R) / 2 [/math] — середина текущего диапазона поиска,
  • [math] l = [/math] [math]\mathtt {lcp(array[L], p)} [/math] — длина общего префикса образца и левого края текущего диапазона поиска,
  • [math] r = [/math] [math]\mathtt {lcp(array[R], p)} [/math] — длина общего префикса образца и правого края текущего диапазона поиска,
  • [math] m_l = [/math] [math]\mathtt {lcp(array[L], array[M])} [/math] — длина общего префикса середины текущего диапазона и левого края текущего диапазона поиска,
  • [math] m_r = [/math] [math]\mathtt {lcp(array[M], array[R])} [/math] — длина общего префикса середины текущего диапазона и правого края текущего диапазона поиска.

Алгоритм

Если диапазон ответов не пустой, то у любого суффикса в пределах диапазона ответов есть префикс, который полностью совпадает с образцом.

В самом начале просто посчитаем [math] l[/math] и [math] r [/math] за линейное время с помощью алгоритма Касаи, Арикавы, Аримуры, Ли и Парка, а во время выполнения алгоритма прямой пересчет производиться не будет, изменения будут происходить за [math] O(1) [/math].

Подсчет [math] m_l [/math] и [math] m_r [/math] можно производить за [math] O(1) [/math], если применять алгоритм Фарака-Колтона и Бендера. Любая пара суффиксов [math] array [/math] из диапазона [math] [L, M] [/math] имеет хотя бы [math] m_l [/math] совпадений в префиксах. Аналогично любая пара суффиксов [math] array [/math] из диапазона [math] [M, R] [/math] имеет хотя бы [math] m_r [/math] совпадений в префиксах.

Поиск границ диапазона ответов

Рассмотрим поиск левой границы диапазона ответов [math]\mathtt{answer} [/math]_[math]\mathtt{left}[/math].

Сразу проверим образец с суффиксами по краям исходного диапазона поиска [math] L [/math] и [math] R [/math]: если образец лексикографически больше последнего суффикса [math] array [/math] или меньше первого суффикса, то образец не встречается в строке вовсе и поиск можно прекратить.

[math] \mathtt{answer} [/math]_[math]\mathtt{left}[/math] ищется при помощи бинарного поиска по суффиксному массиву [math] array [/math]. На каждом шаге поиска нам надо определять, на каком отрезке [math] [L, M] [/math] или [math] [M, R] [/math] надо продолжать поиск границы [math] \mathtt{answer} [/math]_[math]\mathtt{left}[/math] . Каждую итерацию бинарного поиска будем сравнивать [math] l [/math] и [math] r [/math]. Если [math] l \geqslant r [/math], то возможно одно из трех:

  1. [math] m_l \gt l [/math]. Это означает, что каждая пара суффиксов из диапазона [math] [L, M] [/math] имеет между собой больше совпадений, чем суффикс с левого края с образцом, поэтому продолжим поиск в диапазоне [math] [M, R] [/math]. Значение [math] l [/math] при этом не меняется, а [math] L = M [/math].
  2. [math] m_l = l [/math]. Это означает, что у каждого суффикса из [math] [L, M] [/math] есть хотя бы [math] l [/math] совпадений с образцом. Проверим суффикс в позиции [math] M [/math], так как с ним совпадений у образца может получиться больше. Начнем сравнивать суффикс в позиции [math] M [/math] начиная с [math] l [/math]-ого символа. Мы либо найдем полное вхождение образца в суффикс, либо на каком-то шаге [math] k [/math] получим несоответствие. В первом случае [math] R = M [/math] и [math] r = |p| [/math], так как мы ищем левую границу диапазона ответов. Во втором случае все зависит от лексикографического несовпадения. Если символ [math] l + k + 1 [/math] у образца меньше, чем у суффикса, то [math] R = M [/math] и [math] r = l + k + 1[/math], иначе [math] L = M [/math] и [math] l = l + k + 1[/math].
  3. [math] m_l \lt l [/math]. Это означает, что совпадений у суффикса с левого края диапазона поиска с образцом больше, чем у суффикса в позиции [math] M [/math]. Очевидно, что поиск надо продолжать между [math] L [/math] и [math] M [/math], то есть [math] R = M [/math], а новое значение [math] r = m_l [/math].

Если [math] l \lt r [/math], то действия аналогичны. Также три случая:

  1. [math] m_r \gt r [/math]. Сдвигаем [math] R [/math] в [math] M [/math]. Значение [math] r [/math] не изменяется.
  2. [math] m_r = r [/math]. Считаем [math]\mathtt {lcp} [/math] для образца и суффикса, стоящего в позиции [math] M [/math], начиная с позиции [math] r [/math].
  3. [math] m_r \lt r [/math]. Сдвигаем [math] L [/math] в [math] M [/math], [math] l = m_r [/math].

Бинарный поиск будет работать до тех пор, пока [math] R - L \gt 1 [/math]. После этого можно присвоить левой границе диапазона ответов [math] \mathtt{answer} [/math]_[math]\mathtt{left} = R [/math] и переходить к поиску правой границы диапазона ответов [math] \mathtt{answer} [/math]_[math]\mathtt{right}[/math] .

Рассуждения при поиске [math] \mathtt{answer} [/math]_[math]\mathtt{right}[/math] аналогичны, только нужно не забыть изменить границы поиска на изначальные [math] L = 0 [/math] и [math] R = |s| - 1 [/math].

Таким образом часть бинарного поиска мы сделаем при сравнении нескольких [math]\mathtt {lcp} [/math] между собой(каждое за [math] O(1) [/math]), а если дойдет до сравнения символов, то любой символ [math] p [/math] сравнивается не более одного раза(при сравнении мы берем [math]\mathtt {max}[/math][math](l, r) [/math], а значит никогда не возвращаемся назад). В самом начале мы посчитали [math] l [/math] и [math] r [/math] за [math] O(p) [/math]. В итоге получаем сложность алгоритма [math] O(p + log(s)) [/math]. Правда нужен предподсчет, чтобы можно было брать [math]\mathtt {lcp} [/math] для двух любых суффиксов [math] array [/math] за [math] O(1) [/math], начиная с позиции [math] r [/math].

Рисунки

Черная вертикальная линия на рисунке обозначает [math]\mathtt {lcp} [/math] от [math] i [/math]-го суффикса суффиксного массива [math] array [/math] и образца [math] p [/math]. Чем линия длиннее, тем совпадений символов больше.

[math] L [/math], [math] M [/math] и [math] R [/math] — то же самое, что в алгоритме. Кроме того, самая левая черная вертикальная линия на каждом рисунке означает [math] l [/math], аналогично, самая правая черная вертикальная линия на каждом рисунке означает [math] r[/math].

Переменная [math] m_l [/math] — это [math]\mathtt {lcp} [/math] в суффиксном массиве на промежутке [math] [L, M] [/math]. Переменная [math] m_r [/math] — это [math]\mathtt {lcp} [/math] в суффиксном массиве на промежутке [math] [M, R] [/math]. Серым цветом выделен [math]\mathtt {lcp} [/math] в суффиксном массиве на рассматриваемом промежутке.

Иллюстраци возможных случаев при [math] l \geqslant r [/math]:

Left.png

Иллюстрации возможных случаев при [math] l \lt r [/math]:

Right2.png

Псевдокод

Массивы и строки нумеруются с нуля.

Сравнения [math]\lt _z , \gt _z , =_z , \leqslant_z , \geqslant_z [/math] означают лексикографическое сравнение двух строк по их первым [math]z[/math] символам.

Сравнения [math]\lt , \gt , == , \leqslant , \geqslant [/math] при применении к строкам означают полное лексикографическое сравнение строк.

Функция [math]\mathtt {common(z,s, p)}[/math] ищет количество совпадений символов строк [math]s[/math] и [math]p[/math] начиная с позиции [math]z[/math].

[math]n[/math] — длина строки [math]s[/math], [math]w[/math] — длина строки [math]p[/math].

В алгоритме используются переменные, введенные выше в разделе "более быстрый поиск".


Поиск левой границы ответов [math] answer [/math]_[math]left[/math].

function find_answer_left(p: String, s: String): int
    l = lcp(p, s[array[0]])
    r = lcp(p, s[array[n - 1]])
    if (l == w or p < s[array[0]])
        answer_left = 0 
    else if (p > s[array[n - 1])
        answer_left = n
    else 
        L = 0
        R = n - 1
        while (R - L > 1) do 
            M = (L + R) / 2
            m_l = lcp(array[L], array[M])
            m_r = lcp(array[M], array[R])
            if (l [math]\geqslant[/math] r)
                if (m_l [math]\geqslant[/math] l)
                    m = l + common(l, s[array[M]], p)
                else
                    m = m_l
            else
                if (m_r [math]\geqslant[/math] r)
                    m = r + common(r, s[array[M]], p)
                else
                    m = m_r
            if (m == w || p [math]\leqslant[/math][math]_m[/math] s[array[M]]){
                R = M
                r = m
            else 
                L = M
                l = m
        answer_left = R

См. также

Источники информации