Сжатое суффиксное дерево — различия между версиями
(pseudocode correction and some other small fixes) |
Genyaz (обсуждение | вклад) (Добавлено получение суффиксного дерева из суффиксного массива) |
||
Строка 43: | Строка 43: | ||
==Построение суффиксного дерева== | ==Построение суффиксного дерева== | ||
+ | |||
+ | ===Наивный алгоритм=== | ||
Рассмотрим наивный алгоритм построения суффиксного дерева строки <tex>s</tex>: | Рассмотрим наивный алгоритм построения суффиксного дерева строки <tex>s</tex>: | ||
go[0] = new Vertex() //корень | go[0] = new Vertex() //корень | ||
Строка 83: | Строка 85: | ||
Этот алгоритм работает за время <tex>O(n^2)</tex>, однако [[Алгоритм Укконена| алгоритм Укконена]] позволяет построить сжатое суффиксное дерево за <tex>O(n)</tex>. | Этот алгоритм работает за время <tex>O(n^2)</tex>, однако [[Алгоритм Укконена| алгоритм Укконена]] позволяет построить сжатое суффиксное дерево за <tex>O(n)</tex>. | ||
+ | |||
+ | ===Построение из суффиксного массива=== | ||
+ | Пусть нам известен [[Суффиксный массив| суффиксный массив]] <tex>suf</tex> строки <tex>s</tex>, его можно получить [[Алгоритм Карккайнена-Сандерса| алгоритмом Карккайнена-Сандерса]] за линейное время. Для преобразования нам также понадобится массив <tex>lcp</tex> (longest common prefix), который можно получить [[Алгоритм Касаи и др.| алгоритмом Касаи]]. | ||
+ | |||
+ | В этом преобразовании используется тот же инвариант, что и в других суффиксных структурах: | ||
+ | # Строка <tex>s</tex> заканчивается специальным символом, который больше не встречается в строке. | ||
+ | # (Следствие) <tex>lcp[i] < len[i - 1]</tex>, где <tex>len[i - 1]</tex> {{---}} длина суффикса, соответствующего <tex>suf[i - 1]</tex>. | ||
+ | |||
+ | В вершинах дерева мы будем хранить предка <tex>parent</tex>, [[Стек| стек]] детей в лексикографическом порядке ребер <tex>children</tex>, глубину вершины в символах от корня <tex>depth</tex>. | ||
+ | |||
+ | Будем добавлять суффиксы в порядке, заданном суффиксным массивом <tex>suf</tex>. Также будем запоминать последнюю добавленную вершину <tex>previous</tex>. Тогда каждый <tex>i</tex>-ый добавленный суффикс будет иметь с предыдущим <tex>lcp[i]</tex> общих символов, что и будет использоваться для быстрого добавления. | ||
+ | |||
+ | <code> | ||
+ | '''Node'''('''Node''' parent, '''int''' depth) | ||
+ | |||
+ | '''Node''' addNextSuffix('''Node''' previous, '''int''' length, '''int''' lcp) | ||
+ | '''if''' previous.depth == 0 '''or''' previous.depth == lcp <font color=green> // Добавляем к сыновьям текущей вершины </font> | ||
+ | added = '''Node'''(previous, length) | ||
+ | previous.children.'''push'''(added) | ||
+ | '''return''' added | ||
+ | '''else''' | ||
+ | '''if''' previous.parent.depth < lcp <font color=green> // Нужно разрезать ребро </font> | ||
+ | inserted = '''Node'''(prevous.parent, lcp) | ||
+ | previous.parent.children.'''pop'''() | ||
+ | previous.parent.children.'''push'''(inserted) | ||
+ | inserted.children.'''push'''(previous) | ||
+ | previous.parent = inserted | ||
+ | '''return''' addNextSuffix(previous.parent, length, lcp) | ||
+ | |||
+ | '''Node''' buildSuffixTree('''int[]''' suf, '''int[]''' lcp, '''int''' length) | ||
+ | root = '''Node'''('''null''', 0) | ||
+ | previous = root | ||
+ | '''for''' i = 1 '''to''' length | ||
+ | previous = addNextSuffix(previous, length - suf[i], lcp[i]) | ||
+ | '''return''' root | ||
+ | </code> | ||
+ | |||
+ | В процессе построения мы нигде не запоминали сами позиции строки, соответствующие ребрам. Чтобы их восстановить, достаточно определить максимальный суффикс, который проходит по этому ребру. Для этого с помощью [[Обход в глубину, цвета вершин| обхода в глубину]] посчитаем для каждой вершину дерева максимальную глубину ее листа <tex>maxDepth</tex>. | ||
+ | |||
+ | Тогда ребро <tex>s[start, end]</tex> определяется так: | ||
+ | |||
+ | <code> | ||
+ | calculatePositions('''Node''' parent, '''Node''' child, '''int''' stringLength) | ||
+ | start = stringLength - child.maxDepth + parent.depth | ||
+ | end = start + child.depth - parent.depth - 1 | ||
+ | </code> | ||
+ | |||
+ | Для асимптотического анализа будем использовать в качестве [[Амортизационный анализ#Метод потенциалов| потенциала]] глубину в вершинах. При добавлении суффикса мы спускаемся один раз, подняться выше корня мы не можем, значит и подниматься мы будем суммарно <tex>O(n)</tex> раз. Обход в глубину также выполняется за <tex>O(n)</tex>, итоговая асимптотика <tex>O(n)</tex>. | ||
+ | |||
+ | Таким образом, мы умеем за <tex>O(n)</tex> строить [[Алгоритм Укконена| суффиксное дерево]], [[Алгоритм Карккайнена-Сандерса| суффиксный массив]] и преобразовывать одно в другое. Это говорит об эквивалентности этих структур, и мы можем выбирать нужное представление исходя из требуемых свойств. | ||
==Использование сжатого суффиксного дерева== | ==Использование сжатого суффиксного дерева== |
Версия 10:50, 1 мая 2014
Суффиксный бор — удобная структура данных для поиска подстроки в строке, но она требует порядка квадрата длины исходной строки памяти. Оптимизацией суффиксного бора, требующей линейное количество памяти, является сжатое суффиксное дерево рассматриваемое далее.
Содержание
Определение
Определение: |
Суффиксное дерево (сжатое суффиксное дерево)
| для строки (где ) — дерево с листьями, обладающее следующими свойствами:
Данное определение порождает следующую проблему:
Рассмотрим дерево для строки : суффикс является префиксом суффикса , а, значит, этот суффикс не закачивается в листе. Для решения проблемы в конце строки добавляют символ, не входящий в исходный алфавит: защитный символ. Обозначим его как . Любой суффикс строки с защитным символом действительно заканчивается в листе и только в листе, т. к. в такой строке не существует двух различных подстрок одинаковой длины, заканчивающихся на .
Далее
— длина строки с защитным символом.Количество вершин
По определению, в суффиксном дереве содержится
листьев. Оценим количество внутренних вершин такого дерева.Лемма: |
Количество внутренних вершин дерева, каждая из которых имеет не менее двух детей, меньше количества листьев. |
Доказательство: |
Докажем лемму индукцией по количеству листьев .База При в дереве одна внутренняя вершина, следовательно утверждение верно.Переход Возьмем вершину в дереве с листами, у которой два ребенка — листья. Рассмотрим возможные случаи:1) У нее более двух детей. Тогда отрежем от нее лист. Получим дерево с 2) У нее ровно два ребенка. Отрежем их, получим дерево с листьями, причем в нем количество внутренних вершин такое же, как в исходном дереве. Но у полученного дерева по индукционному предположению менее внутренних вершин, а, значит, и для исходного дерева лемма верна. листьями, количество внутренних вершин которого на меньше, чем в исходном дереве. Тогда по индукционному предположению у него менее внутренних вершин, значит, в исходном дереве их меньше . |
Занимаемая память
Представим дерево как двумерный массив размера
, где — количество вершин в дереве, — мощность алфавита. Для любого суффиксного дерева верна предыдущая лемма (у каждой вершины, по определению, не менее двух детей), значит, . Каждая ячейка содержит информацию о том, в какую вершину ведет ребро из -ой вершины по -ому символу и индексы начала и конца подстроки, записанной на данном переходе. Итак, дерево занимает памяти.Построение суффиксного дерева
Наивный алгоритм
Рассмотрим наивный алгоритм построения суффиксного дерева строки
:go[0] = new Vertex() //корень count = 0 //номер последней вершины, созданной в дереве (глобальная переменная) for i = 0 to n //для каждого символа строки insert(i, n) //добавляем суффикс, начинающийся с него
insert(l, r)
cur = 0
while (l < r)
if go[cur][s[l]].v == -1 then //если мы не можем пойти из вершины по символу
createVertex(cur, l, r) //создаем новую вершину
else
start = go[cur][s[l]].l
finish = go[cur][s[l]].r
hasCut = false
for j = start to finish //для каждого символа на ребре из текущей вершины
if s[l+j-start] <> s[j] then //если нашли не совпадающий символ
//создаем вершину на ребре
old = go[cur][s[l]]
createVertex(cur, l, j - 1)
go[count][s[j]].v = old
go[count][s[j]].r = j
go[count][s[j]].l = finish
createVertex(count, l + j - start, r)
hasCut = true
break
if !hasCut then
cur = go[cur][s[l]].v //переходим по ребру
l = l + finish - start //двигаемся по суффиксу на длину подстроки, записанной на ребре
else
break
createVertex(cur, l, r) go[++count] = new Vertex() go[cur][s[l]].v = count go[cur][s[l]].l = l go[cur][s[l]].r = r
Этот алгоритм работает за время , однако алгоритм Укконена позволяет построить сжатое суффиксное дерево за .
Построение из суффиксного массива
Пусть нам известен суффиксный массив строки , его можно получить алгоритмом Карккайнена-Сандерса за линейное время. Для преобразования нам также понадобится массив (longest common prefix), который можно получить алгоритмом Касаи.
В этом преобразовании используется тот же инвариант, что и в других суффиксных структурах:
- Строка заканчивается специальным символом, который больше не встречается в строке.
- (Следствие) , где — длина суффикса, соответствующего .
В вершинах дерева мы будем хранить предка стек детей в лексикографическом порядке ребер , глубину вершины в символах от корня .
,Будем добавлять суффиксы в порядке, заданном суффиксным массивом
. Также будем запоминать последнюю добавленную вершину . Тогда каждый -ый добавленный суффикс будет иметь с предыдущим общих символов, что и будет использоваться для быстрого добавления.
Node(Node parent, int depth) Node addNextSuffix(Node previous, int length, int lcp) if previous.depth == 0 or previous.depth == lcp // Добавляем к сыновьям текущей вершины added = Node(previous, length) previous.children.push(added) return added else if previous.parent.depth < lcp // Нужно разрезать ребро inserted = Node(prevous.parent, lcp) previous.parent.children.pop() previous.parent.children.push(inserted) inserted.children.push(previous) previous.parent = inserted return addNextSuffix(previous.parent, length, lcp) Node buildSuffixTree(int[] suf, int[] lcp, int length) root = Node(null, 0) previous = root for i = 1 to length previous = addNextSuffix(previous, length - suf[i], lcp[i]) return root
В процессе построения мы нигде не запоминали сами позиции строки, соответствующие ребрам. Чтобы их восстановить, достаточно определить максимальный суффикс, который проходит по этому ребру. Для этого с помощью обхода в глубину посчитаем для каждой вершину дерева максимальную глубину ее листа .
Тогда ребро
определяется так:
calculatePositions(Node parent, Node child, int stringLength) start = stringLength - child.maxDepth + parent.depth end = start + child.depth - parent.depth - 1
Для асимптотического анализа будем использовать в качестве потенциала глубину в вершинах. При добавлении суффикса мы спускаемся один раз, подняться выше корня мы не можем, значит и подниматься мы будем суммарно раз. Обход в глубину также выполняется за , итоговая асимптотика .
Таким образом, мы умеем за суффиксное дерево, суффиксный массив и преобразовывать одно в другое. Это говорит об эквивалентности этих структур, и мы можем выбирать нужное представление исходя из требуемых свойств.
строитьИспользование сжатого суффиксного дерева
Суффиксное дерево позволяет за линейное время найти:
- Количество различных подстрок данной строки
- Наибольшую общую подстроку двух строк
- Суффиксный массив и массив (longest common prefix) исходной строки
Источники
- Дэн Гасфилд — Строки, деревья и последовательности в алгоритмах: Информатика и вычислительная биология — СПб.: Невский Диалект; БХВ-Петербург, 2003. — 654 с: ил.