Алгоритм Карккайнена-Сандерса — различия между версиями
(→Алгоритм skew) |
(→Алгоритм) |
||
Строка 15: | Строка 15: | ||
Получили асимптотическое уравнение <tex> T(n) = T(\frac{n}{2}) + O(n) </tex>, решением которого является <tex> T(n) = O(n) </tex>. | Получили асимптотическое уравнение <tex> T(n) = T(\frac{n}{2}) + O(n) </tex>, решением которого является <tex> T(n) = O(n) </tex>. | ||
− | == Алгоритм == | + | == Алгоритм «разделяй и властвуй» == |
Для упрощения алгоритма вначале дополним нашу строку до четной длины (например, добавлением $ в конец). На шаге сливания мы сможем избавиться от него. | Для упрощения алгоритма вначале дополним нашу строку до четной длины (например, добавлением $ в конец). На шаге сливания мы сможем избавиться от него. | ||
=== Шаг 1 === | === Шаг 1 === | ||
− | На первом шаге мы строим суффиксный массив <tex> A_{ | + | На первом шаге мы строим суффиксный массив <tex> A_{S_o} </tex> для нечетных суффиксов строки <tex> S </tex>. |
# Отобразим исходную строку <tex> S </tex> длины <tex> n </tex> в строку <tex> S' </tex> длины <tex> \frac{n}{2} </tex> следующим образом: | # Отобразим исходную строку <tex> S </tex> длины <tex> n </tex> в строку <tex> S' </tex> длины <tex> \frac{n}{2} </tex> следующим образом: | ||
− | #* Сделаем список, состоящий из пар символов вида <tex> S[ | + | #* Сделаем список, состоящий из пар символов вида <tex> S[i..i + 1] </tex>, где <tex> i \mod 2 == 1 </tex>, причем обозначим <tex> S[n-1..n] </tex> как <tex> S[n-1]\$</tex>. |
#* Отсортируем его цифровой сортировкой за линейное время и получим новый алфавит <tex> \Sigma' </tex>. | #* Отсортируем его цифровой сортировкой за линейное время и получим новый алфавит <tex> \Sigma' </tex>. | ||
#* Перекодируем строку <tex> S </tex> в алфавит <tex> \Sigma' </tex>, получив строку <tex> S' </tex> половинной длины. | #* Перекодируем строку <tex> S </tex> в алфавит <tex> \Sigma' </tex>, получив строку <tex> S' </tex> половинной длины. | ||
# Рекурсивно построим суффиксный массив <tex> A_{S'} </tex>. | # Рекурсивно построим суффиксный массив <tex> A_{S'} </tex>. | ||
− | # Построим суффиксный массив <tex> A_{ | + | # Построим суффиксный массив <tex> A_{S_o} </tex>. Очевидно, <tex> A_{S_o}[i] = 2 A_{S'}[i] + 1 </tex>, так отношение упорядоченности любых двух строк в старом алфавите <tex> \Sigma </tex> эквивалентно отношению упорядоченности в новом алфавите <tex> \Sigma' </tex> по его построению. |
=== Шаг 2 === | === Шаг 2 === | ||
− | На этом шаге мы за линейное время получим суффиксный массив <tex> A_{ | + | На этом шаге мы за линейное время получим суффиксный массив <tex> A_{S_e} </tex> для четных суффиксов, используя уже построенный <tex> A_{S_o} </tex>. |
− | Заметим, что сортировка множества | + | Заметим, что сортировка множества четных суффиксов <tex> \{ S[i..n] | i \mod 2 == 0 \} </tex> аналогична сортировке множества пар <tex> \{ (S[i], S[i+1..n]) | i \mod 2 == 0 \} </tex>. Однако <tex> S[i+1..n] </tex> — нечетный суффикс, и его относительную позицию мы уже узнали на шаге 1. |
− | Таким образом, чтобы отсортировать эти пары за линейное время, сначала сразу выпишем их в порядке возрастания второго элемента пары (то есть в порядке вхождения в массив <tex> A_{ | + | Таким образом, чтобы отсортировать эти пары за линейное время, сначала сразу выпишем их в порядке возрастания второго элемента пары (то есть в порядке вхождения в массив <tex> A_{S_o} </tex>), а потом отсортируем устойчивой сортировкой подсчетом по первым элементам. Псевдокод этого шага: |
M = [] | M = [] | ||
− | |||
for i = 0..n/2 - 1: | for i = 0..n/2 - 1: | ||
− | + | M.add(Pair(S[<tex> A_{S_o}</tex>[i] - 1], <tex> A_{S_o}</tex>[i])) | |
− | |||
− | |||
− | |||
− | Заметим, что массив <tex> M </tex> явно не отсортирован по вторым элементам и хранит не суффиксы, а их позиции в строке <tex> S </tex>, но главное — что он отсортирован по возрастанию соответствующих этим позициям | + | Заметим, что массив <tex> M </tex> явно не отсортирован по вторым элементам и хранит не суффиксы, а их позиции в строке <tex> S </tex>, но главное — что он отсортирован по возрастанию соответствующих этим позициям нечетным суффиксам. После устойчивой сортировки массива <tex> M </tex> подсчетом по первому элементу легко восстановить массив <tex> A_{S_e} </tex>: |
− | <tex> A_{ | + | stable_sort(M) |
+ | <tex> A_{S_e} </tex> = [] | ||
for i = 0..n/2 - 1: | for i = 0..n/2 - 1: | ||
− | <tex> A_{ | + | <tex> A_{S_e} </tex>.add(M[i].second - 1) |
Строка 54: | Строка 51: | ||
=== Пример === | === Пример === | ||
− | Покажем первые два шага агоритма для строки ''' | + | Покажем первые два шага агоритма для строки '''bbaaabab'''. |
− | Во-первых, добавим защитный символ $, получив строку ''' | + | Во-первых, добавим защитный символ $, получив строку '''bbaaabab$'''. Во-вторых, дополним ее до четной длины, получив '''bbaaabab$$'''. |
==== Шаг 1 ==== | ==== Шаг 1 ==== | ||
− | # В новом алфавите <tex> \Sigma' </tex> будет | + | # В новом алфавите <tex> \Sigma' </tex> будет четыре элемента — '''ba''', '''aa''', '''b$''', '''$$'''. После сортировки они получат номера 3, 1, 2 и 0 соответственно. |
− | # Сжатой строкой <tex> S' </tex> будет ''' | + | # Сжатой строкой <tex> S' </tex> будет '''31320'''. |
− | # После рекурсивного вызова получим, что <tex> A_{S'} </tex> = [ | + | # После рекурсивного вызова получим, что <tex> A_{S'} </tex> = [4, 1, 3, 0, 2], и <tex> A_{S_e} </tex> = [9, 3, 7, 1, 5]. |
==== Шаг 2 ==== | ==== Шаг 2 ==== | ||
− | # Обойдя массив <tex> A_{ | + | # Обойдя массив <tex> A_{S_o} </tex>, получим M = [('''$''', 9), ('''a''', 3), ('''a''', 7), ('''b''', 1), ('''a''', 5)]. |
− | # После сортировки подсчетом по первому элементу, получим M = [('''$''', | + | # После сортировки подсчетом по первому элементу, получим M = [('''$''', 9), ('''a''', 3), ('''a''', 7), ('''a''', 5), ('''b''', 1)]. |
− | # Восстановив массив <tex> A_{ | + | # Восстановив массив <tex> A_{S_e} </tex>, получаем [8, 2, 6, 4, 0], что действительно является суффиксным массивом для четных суффиксов. |
== Алгоритм Каркайнена-Сандерса == | == Алгоритм Каркайнена-Сандерса == |
Версия 22:50, 29 марта 2012
Алгоритм Каркайнена-Сандерса (Karkkainen, Sanders) — алгоритм построения суффиксного массива за линейное время.
Определение: |
Четным суффиксом назовем суффикс, начинающийся в четной позиции. Нечетным суффиксом — суффикс, начинающийся в нечетной позиции. |
Содержание
Базовая идея
Алгоритм базируется на алгоритме Фараха[1] построения суффиксного дерева за линейное время:
- Строим суффиксное дерево для четных суффиксов рекурсивно сведя задачу к построению суффиксного дерева для строки половинной длины.
- Строим суффиксное дерево для нечетных суффиксов за линейное время, используя результат для четных позиций.
- Сливаем суффиксные деревья за линейное время.
Получили асимптотическое уравнение
, решением которого является .Алгоритм «разделяй и властвуй»
Для упрощения алгоритма вначале дополним нашу строку до четной длины (например, добавлением $ в конец). На шаге сливания мы сможем избавиться от него.
Шаг 1
На первом шаге мы строим суффиксный массив
для нечетных суффиксов строки .- Отобразим исходную строку
- Сделаем список, состоящий из пар символов вида , где , причем обозначим как .
- Отсортируем его цифровой сортировкой за линейное время и получим новый алфавит .
- Перекодируем строку в алфавит , получив строку половинной длины.
длины в строку длины следующим образом:
- Рекурсивно построим суффиксный массив .
- Построим суффиксный массив . Очевидно, , так отношение упорядоченности любых двух строк в старом алфавите эквивалентно отношению упорядоченности в новом алфавите по его построению.
Шаг 2
На этом шаге мы за линейное время получим суффиксный массив
для четных суффиксов, используя уже построенный .Заметим, что сортировка множества четных суффиксов
аналогична сортировке множества пар . Однако — нечетный суффикс, и его относительную позицию мы уже узнали на шаге 1.Таким образом, чтобы отсортировать эти пары за линейное время, сначала сразу выпишем их в порядке возрастания второго элемента пары (то есть в порядке вхождения в массив
), а потом отсортируем устойчивой сортировкой подсчетом по первым элементам. Псевдокод этого шага:M = [] for i = 0..n/2 - 1: M.add(Pair(S[[i] - 1], [i]))
Заметим, что массив
явно не отсортирован по вторым элементам и хранит не суффиксы, а их позиции в строке , но главное — что он отсортирован по возрастанию соответствующих этим позициям нечетным суффиксам. После устойчивой сортировки массива подсчетом по первому элементу легко восстановить массив :stable_sort(M)= [] for i = 0..n/2 - 1: .add(M[i].second - 1)
Получили, что весь второй шаг требует времени.
Шаг 3
Для суффиксного дерева третий шаг алгоритма опирается на специфические особенности суффиксных деревьев, которые не присущи суффиксным массивам. В случае суффиксного массива слияние становится очень сложным [2]. Однако простой модификацией алгоритма можно значительно упростить его.
Пример
Покажем первые два шага агоритма для строки bbaaabab.
Во-первых, добавим защитный символ $, получив строку bbaaabab$. Во-вторых, дополним ее до четной длины, получив bbaaabab$$.
Шаг 1
- В новом алфавите будет четыре элемента — ba, aa, b$, $$. После сортировки они получат номера 3, 1, 2 и 0 соответственно.
- Сжатой строкой будет 31320.
- После рекурсивного вызова получим, что = [4, 1, 3, 0, 2], и = [9, 3, 7, 1, 5].
Шаг 2
- Обойдя массив , получим M = [($, 9), (a, 3), (a, 7), (b, 1), (a, 5)].
- После сортировки подсчетом по первому элементу, получим M = [($, 9), (a, 3), (a, 7), (a, 5), (b, 1)].
- Восстановив массив , получаем [8, 2, 6, 4, 0], что действительно является суффиксным массивом для четных суффиксов.
Алгоритм Каркайнена-Сандерса
Изменим изначальный алгоритм следующим образом:
- Построим суффиксный массив для суффиксов, соответствующих не кратным трем позициям. Рекурсивно сведем это к построению суффиксного массива для строки длиной в две трети исходной.
- Построим суффиксный массив для суффиксов, соответствующих кратных трем позициям, используя результат первого шага за линейное время.
- Сливаем эти суффиксные массивы в один за линейное время.
Получили асимптотическое уравнение
, решением которого также является (это видно из того, что сумма геометрической прогрессии с основанием равна ).Аналогично первой версии алгоритма, дополним строку
до длины, кратной трем, защитными символами .Шаг 1
На этом шаге строится суффиксный массив
для множества суффиксов .- Получим строку
- Сделаем список, состоящий из троек , причем примем , а .
- Отсортируем его за линейное время цифровой сортировкой и получим новый алфавит .
- Перекодируем строку в строку длиной в алфавите следущим образом: . Суффиксу в старом алфавите, где , в новом алфавите будет соответствовать строка , а если , то строка .
аналогично предыдущему алгоритму:
- Вызовем алгоритм рекурсивно для строки , получив суффиксный массив .
- Пройдем по массиву . Если , то этот суффикс соответствует позиции в строке , если же , то этот суффикс соответствует позиции в строке . Псевдокод получения :
= [] for i = 0.. .length - 1: if [i] < n / 3: .add(3 * [i] + 1) else: .add(3 * ( [i] - n / 3) + 2)
Шаг 2
Этот шаг также аналогичен первой версии алгоритма. Сортировка множества
аналогична сортировке пар , где — суффиксы в позициях, равных 1 по модулю 3, относительный порядок которых уже известен. Выпишем эти пары в порядке вхождения их в и отсортируем по первому элементу устойчивой сортировкой подсчетом, получив суффиксный массив . Псевдокод этого шага:= [] M = [] for i = 0..2n/3 - 1: if [i] % 3 == 1: M.add(Pair(S[A_{S_{12}}</tex>[i] - 1], A_{S_{12}}</tex>[i])) stable_sort(M) for i = 0..n/3 - 1: .add(M[i].second - 1)
Аналогично, второй шаг требует
времени.Шаг 3
На этом шаге мы должны слить суффиксные массивы
и , чтобы получить суффиксный массив для всей строки .Применим стандартный алгоритм слияния двух отсортированных массивов. Заметим, что явно массивы не отсортированы, но сотвествующие элементам массива суффиксы — отсортированы.
Пусть на какой-то итерации слияния мы сравниваем суффиксы, соответствующие позициям
, равной 1 по модулю 3, и (она всегда будет равна 0 по модулю 3). Это аналогично сравнению пар и . Сравнить первые элементы пар мы можем за , а относительный порядок вторых элементов пар нам уже известен, так как они соотвествуют позициям, равным 2 и 1 по модулю 3 соответственно.Аналогично, пусть на какой-то итерации слияния мы сравниваем суффиксы, соответствующие позициям
, равной 2 по модулю 3, и (она всегда будет равна 0 по модулю 3). Тогда это аналогично сравнению троек и , что аналогично можно делать за .Псевдокод этой фазы:
= [] // Вначале предподсчитаем за O(n) обратные перестановки для суффиксных массивов, то есть массивы Order такие, что A[Order[i]] = i. // Тогда мы сможем за O(1) сравнивать суффиксы по их позиции. Order12 = inverse( ) Order0 = inverse( ) while i < 2 * n / 3 and j < n / 3: pos12 = [i] pos0 = [j] if pos12 % 3 == 1: if Pair(S[pos12], Order12[pos12 + 1]) < Pair(S[pos0], Order0[pos0 + 1]): .add(pos12) i++ else: .add(pos0) j++ else: if Triple(S[pos12], S[pos12 + 1], Order12[pos12 + 2]) < Triple(S[pos0], S[pos0 + 1], Order0[pos0 + 2]): .add(pos12) i++ else: .add(pos0) j++ while i < 2 * n / 3: .add( [i]) i++ while j < n / 3: .add( [j]) i++
Таким образом, получили простой метод сливания за
.Ссылки
- ↑ M. Farach. Optimal suffix tree construction with large alphabets. http://www.cs.rutgers.edu/~farach/pubs/FarFerrMuthu00.pdf
- ↑ D. K. Kim, J. S. Sim, H. Park, and K. Park. Linear-time construction of suffix arrays. http://www.springerlink.com/content/568156021q45r320/