Алгоритм Карккайнена-Сандерса — различия между версиями
м (переименовал Алгоритм Каркайнена-Сандерса в Алгоритм Карккайнена-Сандерса) |
|||
| Строка 1: | Строка 1: | ||
Алгоритм Карккайнена-Сандерса (Karkkainen, Sanders) — алгоритм построения [[суффиксный массив | суффиксного массива]] за линейное время. | Алгоритм Карккайнена-Сандерса (Karkkainen, Sanders) — алгоритм построения [[суффиксный массив | суффиксного массива]] за линейное время. | ||
| − | + | __TOC__ | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | == Используемые обозначения == | |
| + | * В данном конспекте используется 0-индексация. | ||
| + | * <tex>S[i..j] </tex> — подстрока строки <tex> S </tex> с <tex>i</tex>-го по <tex>j</tex>-й символы включительно. | ||
| + | * Пусть длина строки <tex> S </tex> равна <tex> n </tex>. Обозначим <tex> S[i..j] </tex>, где <tex> j \ge n </tex> как строку <tex> S[i..n-1] </tex>, дополненную защитными символами <tex> \$ </tex> до длины <tex> n </tex>. | ||
== Алгоритм «разделяй и властвуй» == | == Алгоритм «разделяй и властвуй» == | ||
| + | |||
{{Определение | {{Определение | ||
|definition= | |definition= | ||
| Строка 15: | Строка 15: | ||
'''Нечетным суффиксом''' — суффикс, начинающийся в нечетной позиции. | '''Нечетным суффиксом''' — суффикс, начинающийся в нечетной позиции. | ||
}} | }} | ||
| + | |||
| + | # Строим суффиксный массив для нечетных суффиксов, рекурсивно сведя задачу к построению суффиксного массива для строки половинной длины. | ||
| + | # Строим суффиксный массив для четных суффиксов за линейное время, используя результат первого шага. | ||
| + | # Сливаем суффиксные массивы за линейное время. | ||
| + | |||
| + | Получили асимптотическое уравнение <tex> T(n) = T(\frac{n}{2}) + O(n) </tex>, решением которого является <tex> T(n) = O(n) </tex>. | ||
Для упрощения алгоритма вначале дополним нашу строку до четной длины (например, добавлением $ в конец). На шаге слияния мы сможем избавиться от него. | Для упрощения алгоритма вначале дополним нашу строку до четной длины (например, добавлением $ в конец). На шаге слияния мы сможем избавиться от него. | ||
| + | |||
| + | === База рекурсии === | ||
| + | Если длина текущей строки <tex> S </tex> меньше двух, надо выполнить обычное сравнение суффиксов. | ||
| + | |||
=== Шаг 1 === | === Шаг 1 === | ||
На первом шаге мы строим суффиксный массив <tex> A_{S_o} </tex> для нечетных суффиксов строки <tex> S </tex>. | На первом шаге мы строим суффиксный массив <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[i..i + 1] </tex>, где <tex> i \mod 2 | + | #* Сделаем список, состоящий из пар символов вида <tex> S[i..i + 1] </tex>, где <tex> i \mod 2 = 1 </tex>. |
#* Отсортируем его цифровой сортировкой за линейное время и получим новый алфавит <tex> \Sigma' </tex>. | #* Отсортируем его цифровой сортировкой за линейное время и получим новый алфавит <tex> \Sigma' </tex>. | ||
| − | #* Перекодируем строку <tex> S </tex> в алфавит <tex> \Sigma' </tex>, получив строку <tex> S' </tex> половинной длины. | + | #* Перекодируем строку <tex> S[1..n] </tex> в алфавит <tex> \Sigma' </tex>, получив строку <tex> S' </tex> половинной длины. |
# Рекурсивно построим суффиксный массив <tex> A_{S'} </tex>. | # Рекурсивно построим суффиксный массив <tex> A_{S'} </tex>. | ||
# Построим суффиксный массив <tex> A_{S_o} </tex>. Очевидно, <tex> A_{S_o}[i] = 2 A_{S'}[i] + 1 </tex>, так отношение упорядоченности любых двух строк в старом алфавите <tex> \Sigma </tex> эквивалентно отношению упорядоченности в новом алфавите <tex> \Sigma' </tex> по его построению. | # Построим суффиксный массив <tex> A_{S_o} </tex>. Очевидно, <tex> A_{S_o}[i] = 2 A_{S'}[i] + 1 </tex>, так отношение упорядоченности любых двух строк в старом алфавите <tex> \Sigma </tex> эквивалентно отношению упорядоченности в новом алфавите <tex> \Sigma' </tex> по его построению. | ||
| Строка 30: | Строка 40: | ||
На этом шаге мы за линейное время получим суффиксный массив <tex> A_{S_e} </tex> для четных суффиксов, используя уже построенный <tex> A_{S_o} </tex>. | На этом шаге мы за линейное время получим суффиксный массив <tex> A_{S_e} </tex> для четных суффиксов, используя уже построенный <tex> A_{S_o} </tex>. | ||
| − | Заметим, что сортировка множества четных суффиксов <tex> \{ S[i..n] | i \mod 2 | + | Заметим, что сортировка множества четных суффиксов <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_{S_o} </tex>), а потом отсортируем устойчивой сортировкой подсчетом по первым элементам. Псевдокод этого шага: | Таким образом, чтобы отсортировать эти пары за линейное время, сначала сразу выпишем их в порядке возрастания второго элемента пары (то есть в порядке вхождения в массив <tex> A_{S_o} </tex>), а потом отсортируем устойчивой сортировкой подсчетом по первым элементам. Псевдокод этого шага: | ||
| Строка 47: | Строка 57: | ||
=== Шаг 3 === | === Шаг 3 === | ||
| − | Для суффиксного дерева третий шаг алгоритма опирается на специфические особенности суффиксных деревьев, которые не присущи суффиксным массивам. | + | Для суффиксного дерева третий шаг алгоритма опирается на специфические особенности суффиксных деревьев, которые не присущи суффиксным массивам<ref>M. Farach. Optimal suffix tree construction with large alphabets. http://www.cs.rutgers.edu/~farach/pubs/FarFerrMuthu00.pdf </ref> . |
В случае суффиксного массива слияние становится очень сложным, но все же оно было реализовано в алгоритме Ким-Сим-Парк-Парка<ref> D. K. Kim, J. S. Sim, H. Park, and K. Park. Linear-time construction of suffix arrays. http://www.springerlink.com/content/568156021q45r320/</ref>. Однако простой модификацией алгоритма можно значительно упростить его. | В случае суффиксного массива слияние становится очень сложным, но все же оно было реализовано в алгоритме Ким-Сим-Парк-Парка<ref> D. K. Kim, J. S. Sim, H. Park, and K. Park. Linear-time construction of suffix arrays. http://www.springerlink.com/content/568156021q45r320/</ref>. Однако простой модификацией алгоритма можно значительно упростить его. | ||
=== Пример === | === Пример === | ||
| − | Покажем первые два шага агоритма для строки ''' | + | Покажем первые два шага агоритма для строки '''ababbbaa'''. |
| − | Во-первых, | + | Во-первых, добавив защитный символ '''$''', получив строку '''ababbbaa$''' (для этого алгоритма он не требуется, но может понадобиться в применениях суффиксного массива). Во-вторых, дополним ее до четной длины, получив '''ababbbaa$$'''. |
==== Шаг 1 ==== | ==== Шаг 1 ==== | ||
| − | # В новом алфавите <tex> \Sigma' </tex> будет четыре элемента — '''ba''', ''' | + | # В новом алфавите <tex> \Sigma' </tex> будет четыре элемента — '''ba''', '''bb''', '''a$''', '''$$'''. После сортировки они получат номера 2, 3, 1 и 0 соответственно. |
| − | # Сжатой строкой <tex> S' </tex> будет ''' | + | # Переводим строку S[1..n] = '''babbbaa$$$''' в новый алфавит. Сжатой строкой <tex> S' </tex> будет '''23210'''. |
| − | # После рекурсивного вызова получим, что <tex> A_{S'} </tex> = [4, | + | # После рекурсивного вызова получим, что <tex> A_{S'} </tex> = [4, 3, 2, 0, 1], и <tex> A_{S_o} </tex> = [9, 7, 5, 1, 3]. |
==== Шаг 2 ==== | ==== Шаг 2 ==== | ||
| − | # Обойдя массив <tex> A_{S_o} </tex>, получим <tex> M </tex> = [('''$''', 9), ('''a''', | + | # Обойдя массив <tex> A_{S_o} </tex>, получим <tex> M </tex> = [('''$''', 9), ('''a''', 7), ('''b''', 5), ('''a''', 1), ('''a''', 3)]. |
| − | # После сортировки подсчетом по первому элементу, получим <tex> M </tex>= [('''$''', 9), ('''a''', | + | # После сортировки подсчетом по первому элементу, получим <tex> M </tex>= [('''$''', 9), ('''a''', 7), ('''a''', 1), ('''a''', 3), ('''b''', 5)]. |
| − | # Восстановив массив <tex> A_{S_e} </tex>, получаем [8, 2 | + | # Восстановив массив <tex> A_{S_e} </tex>, получаем [8, 6, 0, 2, 4], что действительно является суффиксным массивом для четных суффиксов. |
| + | |||
| + | ==== Шаг 3 ==== | ||
| + | Если бы мы умели сливать <tex> A_{S_o} </tex> и <tex> A_{S_e} </tex> за линейное время, получили бы: | ||
| + | 9 '''$''' | ||
| + | 8 '''$$''' | ||
| + | 7 '''a$$''' | ||
| + | 6 '''aa$$''' | ||
| + | 0 '''ababbbaa$$''' | ||
| + | 2 '''abbbaa$$''' | ||
| + | 5 '''baa$$''' | ||
| + | 1 '''babbbaa$$''' | ||
| + | 4 '''bbaa$$''' | ||
| + | 3 '''bbbaa$$''' | ||
| + | |||
| + | Как и было сказано вначале, избавиться от лишних '''$''' легко, так как суффиксы, им соответствующие, будут первыми в суффиксном массиве (в данном случае достаточно выбросить "9" из суффиксного массива). | ||
== Алгоритм Карккайнена-Сандерса == | == Алгоритм Карккайнена-Сандерса == | ||
| Строка 74: | Строка 99: | ||
Аналогично первой версии алгоритма, дополним строку <tex> S </tex> до длины, кратной трем, защитными символами <tex> \$ </tex>. | Аналогично первой версии алгоритма, дополним строку <tex> S </tex> до длины, кратной трем, защитными символами <tex> \$ </tex>. | ||
| + | |||
| + | === База рекурсии === | ||
| + | Для этого алгоритма базой рекурсии будет строка длиной меньшей или равной 4, так как строка длины 4 дополнится до строки длины 6, после чего вновь будет рекурсивный вызов строки длиной 4. На этом этапе также можно применить обычную сортировку суффиксов, так как это потребует <tex> O(1) </tex> действий. | ||
=== Шаг 1 === | === Шаг 1 === | ||
На этом шаге строится суффиксный массив <tex> A_{S_{12}} </tex> для множества суффиксов <tex> \{ S[i..n-1] | i \mod 3 \ne 0 \} </tex>. | На этом шаге строится суффиксный массив <tex> A_{S_{12}} </tex> для множества суффиксов <tex> \{ S[i..n-1] | i \mod 3 \ne 0 \} </tex>. | ||
# Получим строку <tex> S' </tex> аналогично предыдущему алгоритму: | # Получим строку <tex> S' </tex> аналогично предыдущему алгоритму: | ||
| − | #* Сделаем список, состоящий из троек <tex> S[i..i+2]</tex> , где <tex> i \mod 3 \ne 0 | + | #* Сделаем список, состоящий из троек <tex> S[i..i+2]</tex> , где <tex> i \mod 3 \ne 0 </tex>. |
#* Отсортируем его за линейное время цифровой сортировкой и получим новый алфавит <tex> \Sigma' </tex>. | #* Отсортируем его за линейное время цифровой сортировкой и получим новый алфавит <tex> \Sigma' </tex>. | ||
| − | #* Перекодируем строку <tex> S </tex> в строку <tex> S' </tex> длиной <tex> \frac23 n </tex> в алфавите <tex> \Sigma' | + | #* Перекодируем строку <tex> S[1..n]S[2..n+1] </tex> в строку <tex> S' </tex> длиной <tex> \frac23 n </tex> в алфавите <tex> \Sigma' </tex>. Тогда суффиксу <tex> S[i..n-1] </tex> в старом алфавите, где <tex> i \mod 3 = 1 </tex>, в новом алфавите будет соответствовать строка <tex> S'[\frac{i-1}{3}..\frac{n}{3} - 1] </tex>, а если <tex> i\mod 3 = 2 </tex>, то строка <tex> S'[\frac{n}{3} + \frac{i-2}{3}..\frac{2n}{3} - 1] </tex>. |
# Вызовем алгоритм рекурсивно для строки <tex> S' </tex>, получив суффиксный массив <tex> A_{S'} </tex>. | # Вызовем алгоритм рекурсивно для строки <tex> S' </tex>, получив суффиксный массив <tex> A_{S'} </tex>. | ||
# Пройдем по массиву <tex> A_{S'} </tex>. Если <tex> A_{S'}[i] < \frac{n}{3} </tex>, то этот суффикс соответствует позиции <tex> j = 3A_{S'}[i] + 1 </tex> в строке <tex> S </tex>, если же <tex> A_{S'}[i] \ge \frac{n}{3} </tex>, то этот суффикс соответствует позиции <tex> j = 3(A_{S'}[i] - \frac{n}{3}) + 2 </tex> в строке <tex> S </tex>. Псевдокод получения <tex> A_{S_{12}} </tex>: | # Пройдем по массиву <tex> A_{S'} </tex>. Если <tex> A_{S'}[i] < \frac{n}{3} </tex>, то этот суффикс соответствует позиции <tex> j = 3A_{S'}[i] + 1 </tex> в строке <tex> S </tex>, если же <tex> A_{S'}[i] \ge \frac{n}{3} </tex>, то этот суффикс соответствует позиции <tex> j = 3(A_{S'}[i] - \frac{n}{3}) + 2 </tex> в строке <tex> S </tex>. Псевдокод получения <tex> A_{S_{12}} </tex>: | ||
| Строка 91: | Строка 119: | ||
=== Шаг 2 === | === Шаг 2 === | ||
| − | Этот шаг также аналогичен первой версии алгоритма. Сортировка множества <tex> \{ S[i..n-1] | i \mod 3 | + | Этот шаг также аналогичен первой версии алгоритма. Сортировка множества <tex> \{ S[i..n-1] | i \mod 3 = 0 \} </tex> аналогична сортировке пар <tex> \{ (S[i], S[i+1..n-1]) | i \mod 3 = 0 \} </tex>, где <tex> S[i+1..n-1] </tex> — суффиксы в позициях, равных 1 по модулю 3, относительный порядок которых уже известен. Выпишем эти пары в порядке вхождения их в <tex> A_{S_{12}} </tex> и отсортируем по первому элементу устойчивой сортировкой подсчетом, получив суффиксный массив <tex> A_{S_0} </tex>. Псевдокод этого шага: |
<tex>A_{S_0}</tex> = [] | <tex>A_{S_0}</tex> = [] | ||
| Строка 148: | Строка 176: | ||
==== Шаг 1 ==== | ==== Шаг 1 ==== | ||
# Тройками, соответствующими равными 1 по модулю 3 позициям, будут: '''bba''', '''cab''', '''$$$''', соответствующими равным 2 по модулю 3 — '''bac''', '''ab$''', '''$$$'''. Новый алфавит <tex> \Sigma' </tex> будет содержать элементы '''bba''', '''cab''', '''$$$''', '''bac''', '''ab$''', которые после сортировки получат номера 3, 4, 0, 2, 1 соотвествтенно. | # Тройками, соответствующими равными 1 по модулю 3 позициям, будут: '''bba''', '''cab''', '''$$$''', соответствующими равным 2 по модулю 3 — '''bac''', '''ab$''', '''$$$'''. Новый алфавит <tex> \Sigma' </tex> будет содержать элементы '''bba''', '''cab''', '''$$$''', '''bac''', '''ab$''', которые после сортировки получат номера 3, 4, 0, 2, 1 соотвествтенно. | ||
| − | # Строкой <tex> S' </tex> | + | # Строкой '''bbacab$$$bacab$$$$''' в новом алфавите <tex> \Sigma' </tex> будет <tex> S' </tex> = '''340210'''. |
# После рекурсивного вызова получим <tex> A_{S'} </tex> = [5, 2, 4, 3, 0, 1]. Пересчитав <tex> A_{S_{12}} </tex>, получим [(5 - 3)*3 + 2, 2 * 3 + 1, (4 - 3) * 3 + 2, (3 - 3) * 3 + 2, 0 * 3 + 1, 1 * 3 + 1] = [8, 7, 5, 2, 1, 4]. | # После рекурсивного вызова получим <tex> A_{S'} </tex> = [5, 2, 4, 3, 0, 1]. Пересчитав <tex> A_{S_{12}} </tex>, получим [(5 - 3)*3 + 2, 2 * 3 + 1, (4 - 3) * 3 + 2, (3 - 3) * 3 + 2, 0 * 3 + 1, 1 * 3 + 1] = [8, 7, 5, 2, 1, 4]. | ||
| Строка 161: | Строка 189: | ||
# В конце итерации получаем <tex> A_{S} </tex> = [8, 7, 5], <tex> i </tex> = 3, <tex> j </tex> = 0. | # В конце итерации получаем <tex> A_{S} </tex> = [8, 7, 5], <tex> i </tex> = 3, <tex> j </tex> = 0. | ||
К концу слияния получим <tex> A_{S} </tex> = [8, 7, 5, 0, 3, 6, 2, 1, 4]. Так как мы добавляли один символ '''$''' в начале алгоритма для дополнения строки до длины, кратной трем, выбросим последний суффикс из <tex> A_{S} </tex>, получим в итоге, что <tex> A_{S} </tex> = [7, 5, 0, 3, 6, 2, 1, 4]. | К концу слияния получим <tex> A_{S} </tex> = [8, 7, 5, 0, 3, 6, 2, 1, 4]. Так как мы добавляли один символ '''$''' в начале алгоритма для дополнения строки до длины, кратной трем, выбросим последний суффикс из <tex> A_{S} </tex>, получим в итоге, что <tex> A_{S} </tex> = [7, 5, 0, 3, 6, 2, 1, 4]. | ||
| + | 8 '''$''' (будет выброшен, так как в начале алгоритма добавляли один '''$''' к строке) | ||
| + | 7 '''$$''' | ||
| + | 5 '''ab$$''' | ||
| + | 0 '''abbacab$$''' | ||
| + | 3 '''acab$$''' | ||
| + | 6 '''b$$''' | ||
| + | 2 '''bacab$$''' | ||
| + | 1 '''bbacab$$''' | ||
| + | 4 '''cab$$''' | ||
== Получение LCP == | == Получение LCP == | ||
| Строка 166: | Строка 203: | ||
== Обобщение алгоритма == | == Обобщение алгоритма == | ||
| − | На самом деле, алгоритм можно обобщить<ref name="generalisation"> Juha Kärkkäinen, Peter Sanders and Stefan Burkhardt. Linear work suffix array construction. http://www.cs.helsinki.fi/juha.karkkainen/publications/jacm05-revised.pdf </ref>, взяв на первом шаге, к примеру, суффиксы, позиции которых по модулю 7 дают 3, 5 и 6. Для этого потребуются некоторое усложнение алгоритма, например, сортировка оставшихся суффиксов в нескольких группах на шаге 2 и | + | На самом деле, алгоритм можно обобщить<ref name="generalisation"> Juha Kärkkäinen, Peter Sanders and Stefan Burkhardt. Linear work suffix array construction. http://www.cs.helsinki.fi/juha.karkkainen/publications/jacm05-revised.pdf </ref>, взяв на первом шаге, к примеру, суффиксы, позиции которых по модулю 7 дают 3, 5 и 6. Для этого потребуются некоторое усложнение алгоритма, например, сортировка оставшихся суффиксов в нескольких группах на шаге 2 и слияние нескольких групп на шаге 3, но основная идея алгоритма остается той же. Множества, которые можно выбрать, на первом шаге определяются '''разностным покрытием''' (''difference cover''). |
{{Определение | {{Определение | ||
|definition= | |definition= | ||
'''Разностное покрытие''' (''difference cover'') <tex> D </tex> по модулю <tex>m </tex> — множество чисел от <tex>0</tex> до <tex>m - 1 </tex> таких, что <tex> \forall i \in [0, m-1]: \exists j, k \in D: i \equiv k - j\ ( \mod m) </tex>. | '''Разностное покрытие''' (''difference cover'') <tex> D </tex> по модулю <tex>m </tex> — множество чисел от <tex>0</tex> до <tex>m - 1 </tex> таких, что <tex> \forall i \in [0, m-1]: \exists j, k \in D: i \equiv k - j\ ( \mod m) </tex>. | ||
}} | }} | ||
| − | Заметим, что <tex> \{1, 2\} </tex> является разностным покрытием по модулю <tex> 3 </tex>, <tex> \{3, 5, 6\} </tex> является разностным покрытием по модулю <tex> 7 </tex>, а <tex> \{1\} </tex> — не является разностным покрытием по модулю <tex> 2 </tex>, поэтому этот алгоритм не применим к нему. Подробнее узнать, как вычислять разностное покрытие для заданного модуля можно также здесь | + | Заметим, что <tex> \{1, 2\} </tex> является разностным покрытием по модулю <tex> 3 </tex>, <tex> \{3, 5, 6\} </tex> является разностным покрытием по модулю <tex> 7 </tex>, а <tex> \{1\} </tex> — не является разностным покрытием по модулю <tex> 2 </tex>, поэтому этот алгоритм не применим к нему. Подробнее узнать, как вычислять разностное покрытие для заданного модуля можно также здесь<ref name="generalisation"/>. |
== Ссылки == | == Ссылки == | ||
<references /> | <references /> | ||
Версия 15:58, 1 апреля 2012
Алгоритм Карккайнена-Сандерса (Karkkainen, Sanders) — алгоритм построения суффиксного массива за линейное время.
Содержание
Используемые обозначения
- В данном конспекте используется 0-индексация.
- — подстрока строки с -го по -й символы включительно.
- Пусть длина строки равна . Обозначим , где как строку , дополненную защитными символами до длины .
Алгоритм «разделяй и властвуй»
| Определение: |
| Четным суффиксом назовем суффикс, начинающийся в четной позиции. Нечетным суффиксом — суффикс, начинающийся в нечетной позиции. |
- Строим суффиксный массив для нечетных суффиксов, рекурсивно сведя задачу к построению суффиксного массива для строки половинной длины.
- Строим суффиксный массив для четных суффиксов за линейное время, используя результат первого шага.
- Сливаем суффиксные массивы за линейное время.
Получили асимптотическое уравнение , решением которого является .
Для упрощения алгоритма вначале дополним нашу строку до четной длины (например, добавлением $ в конец). На шаге слияния мы сможем избавиться от него.
База рекурсии
Если длина текущей строки меньше двух, надо выполнить обычное сравнение суффиксов.
Шаг 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
Для суффиксного дерева третий шаг алгоритма опирается на специфические особенности суффиксных деревьев, которые не присущи суффиксным массивам[1] . В случае суффиксного массива слияние становится очень сложным, но все же оно было реализовано в алгоритме Ким-Сим-Парк-Парка[2]. Однако простой модификацией алгоритма можно значительно упростить его.
Пример
Покажем первые два шага агоритма для строки ababbbaa.
Во-первых, добавив защитный символ $, получив строку ababbbaa$ (для этого алгоритма он не требуется, но может понадобиться в применениях суффиксного массива). Во-вторых, дополним ее до четной длины, получив ababbbaa$$.
Шаг 1
- В новом алфавите будет четыре элемента — ba, bb, a$, $$. После сортировки они получат номера 2, 3, 1 и 0 соответственно.
- Переводим строку S[1..n] = babbbaa$$$ в новый алфавит. Сжатой строкой будет 23210.
- После рекурсивного вызова получим, что = [4, 3, 2, 0, 1], и = [9, 7, 5, 1, 3].
Шаг 2
- Обойдя массив , получим = [($, 9), (a, 7), (b, 5), (a, 1), (a, 3)].
- После сортировки подсчетом по первому элементу, получим = [($, 9), (a, 7), (a, 1), (a, 3), (b, 5)].
- Восстановив массив , получаем [8, 6, 0, 2, 4], что действительно является суффиксным массивом для четных суффиксов.
Шаг 3
Если бы мы умели сливать и за линейное время, получили бы:
9 $ 8 $$ 7 a$$ 6 aa$$ 0 ababbbaa$$ 2 abbbaa$$ 5 baa$$ 1 babbbaa$$ 4 bbaa$$ 3 bbbaa$$
Как и было сказано вначале, избавиться от лишних $ легко, так как суффиксы, им соответствующие, будут первыми в суффиксном массиве (в данном случае достаточно выбросить "9" из суффиксного массива).
Алгоритм Карккайнена-Сандерса
Изменим изначальный алгоритм следующим образом:
- Построим суффиксный массив для суффиксов, соответствующих не кратным трем позициям. Рекурсивно сведем это к построению суффиксного массива для строки длиной в две трети исходной.
- Построим суффиксный массив для суффиксов, соответствующих кратным трем позициям, используя результат первого шага за линейное время.
- Сливаем эти суффиксные массивы в один за линейное время.
Получили асимптотическое уравнение , решением которого также является (это видно из того, что сумма геометрической прогрессии с основанием равна ).
Аналогично первой версии алгоритма, дополним строку до длины, кратной трем, защитными символами .
База рекурсии
Для этого алгоритма базой рекурсии будет строка длиной меньшей или равной 4, так как строка длины 4 дополнится до строки длины 6, после чего вновь будет рекурсивный вызов строки длиной 4. На этом этапе также можно применить обычную сортировку суффиксов, так как это потребует действий.
Шаг 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[[i] - 1], [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) обратную перестановку для суффиксного массива , то есть массив rank такой, что [rank[i]] = i. // Тогда мы сможем за O(1) сравнивать суффиксы по их позиции. rank = inverse() while i < 2 * n / 3 and j < n / 3: pos12 = [i] pos0 = [j] if pos12 % 3 == 1: if Pair(S[pos12], rank[pos12 + 1]) < Pair(S[pos0], rank[pos0 + 1]): .add(pos12) i++ else: .add(pos0) j++ else: if Triple(S[pos12], S[pos12 + 1], rank[pos12 + 2]) < Triple(S[pos0], S[pos0 + 1], rank[pos0 + 2]): .add(pos12) i++ else: .add(pos0) j++ while i < 2 * n / 3: .add([i]) i++ while j < n / 3: .add([j]) i++
Таким образом, получили простой метод слияния за .
Пример
Построим суффиксный массив для строки abbacab. После добавления защитного символа и дополнения до кратной трем длины, получим abbacab$$.
Шаг 1
- Тройками, соответствующими равными 1 по модулю 3 позициям, будут: bba, cab, $$$, соответствующими равным 2 по модулю 3 — bac, ab$, $$$. Новый алфавит будет содержать элементы bba, cab, $$$, bac, ab$, которые после сортировки получат номера 3, 4, 0, 2, 1 соотвествтенно.
- Строкой bbacab$$$bacab$$$$ в новом алфавите будет = 340210.
- После рекурсивного вызова получим = [5, 2, 4, 3, 0, 1]. Пересчитав , получим [(5 - 3)*3 + 2, 2 * 3 + 1, (4 - 3) * 3 + 2, (3 - 3) * 3 + 2, 0 * 3 + 1, 1 * 3 + 1] = [8, 7, 5, 2, 1, 4].
Шаг 2
- Обойдя массив и выбрав в нем элементы, равные 1 по модулю 3, получим массив пар = [(b, 7), (a, 1), (a, 4)]
- После устойчивой сортировки подсчетом по первому элементу, получим = [(a, 1), (a, 4), (b, 7)]
- Восстановив , получаем [0, 3, 6].
Шаг 3
Рассмотрим, к примеру, третью итерацию слияния, к этой итерации массив = [8, 7], = 2, = 0, на ней мы сливаем суффиксы, соответствующие позициям 5 и 0.
- Образуем тройки (S[5], S[6], S[7..8]) и (S[0], S[1], S[2..8]).
- После получения относительного порядка суффиксов, получим тройки (a, b, 1) и (a, b, 3). Первая тройка меньше второй, поэтому добавляем суффикс, соответствующий позиции 5 в массив .
- В конце итерации получаем = [8, 7, 5], = 3, = 0.
К концу слияния получим = [8, 7, 5, 0, 3, 6, 2, 1, 4]. Так как мы добавляли один символ $ в начале алгоритма для дополнения строки до длины, кратной трем, выбросим последний суффикс из , получим в итоге, что = [7, 5, 0, 3, 6, 2, 1, 4].
8 $ (будет выброшен, так как в начале алгоритма добавляли один $ к строке) 7 $$ 5 ab$$ 0 abbacab$$ 3 acab$$ 6 b$$ 2 bacab$$ 1 bbacab$$ 4 cab$$
Получение LCP
LCP можно получить за линейное время алгоритмом Касаи.
Обобщение алгоритма
На самом деле, алгоритм можно обобщить[3], взяв на первом шаге, к примеру, суффиксы, позиции которых по модулю 7 дают 3, 5 и 6. Для этого потребуются некоторое усложнение алгоритма, например, сортировка оставшихся суффиксов в нескольких группах на шаге 2 и слияние нескольких групп на шаге 3, но основная идея алгоритма остается той же. Множества, которые можно выбрать, на первом шаге определяются разностным покрытием (difference cover).
| Определение: |
| Разностное покрытие (difference cover) по модулю — множество чисел от до таких, что . |
Заметим, что является разностным покрытием по модулю , является разностным покрытием по модулю , а — не является разностным покрытием по модулю , поэтому этот алгоритм не применим к нему. Подробнее узнать, как вычислять разностное покрытие для заданного модуля можно также здесь[3].
Ссылки
- ↑ 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/
- ↑ 3,0 3,1 Juha Kärkkäinen, Peter Sanders and Stefan Burkhardt. Linear work suffix array construction. http://www.cs.helsinki.fi/juha.karkkainen/publications/jacm05-revised.pdf