Сжатое суффиксное дерево — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(pseudocode correction and some other small fixes)
(Добавлено получение суффиксного дерева из суффиксного массива)
Строка 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

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

Определение

Определение:
Суффиксное дерево (сжатое суффиксное дерево) [math]T[/math] для строки [math]s[/math] (где [math]|s| = n[/math]) — дерево с [math]n[/math] листьями, обладающее следующими свойствами:
  • Каждая внутренняя вершина дерева имеет не меньше двух детей;
  • Каждое ребро помечено непустой подстрокой строки [math]s[/math];
  • Никаких два ребра, выходящие из одной вершины, не могут иметь пометок, начинающихся с одного и того же символа;
  • Дерево должно содержать все суффиксы строки [math]s[/math], причем каждый суффикс заканчивается точно в листе и нигде кроме него.


Суффиксное дерево для строки [math]xabxa[/math] с защитным символом

Данное определение порождает следующую проблему:
Рассмотрим дерево для строки [math]xabxa[/math]: суффикс [math]xa[/math] является префиксом суффикса [math]xabxa[/math], а, значит, этот суффикс не закачивается в листе. Для решения проблемы в конце строки [math]s[/math] добавляют символ, не входящий в исходный алфавит: защитный символ. Обозначим его как [math]\$[/math]. Любой суффикс строки с защитным символом действительно заканчивается в листе и только в листе, т. к. в такой строке не существует двух различных подстрок одинаковой длины, заканчивающихся на [math]\$[/math].

Далее [math]n[/math] — длина строки [math]s[/math] с защитным символом.

Количество вершин

По определению, в суффиксном дереве содержится [math]n[/math] листьев. Оценим количество внутренних вершин такого дерева.

Лемма:
Количество внутренних вершин дерева, каждая из которых имеет не менее двух детей, меньше количества листьев.
Доказательство:
[math]\triangleright[/math]

Докажем лемму индукцией по количеству листьев [math]n[/math].

База

При [math]n = 2[/math] в дереве одна внутренняя вершина, следовательно утверждение верно.

Переход [math]n \rightarrow n + 1[/math]

Возьмем вершину в дереве с [math]n + 1[/math] листами, у которой два ребенка — листья. Рассмотрим возможные случаи:

1) У нее более двух детей. Тогда отрежем от нее лист. Получим дерево с [math]n[/math] листьями, причем в нем количество внутренних вершин такое же, как в исходном дереве. Но у полученного дерева по индукционному предположению менее [math]n[/math] внутренних вершин, а, значит, и для исходного дерева лемма верна.

2) У нее ровно два ребенка. Отрежем их, получим дерево с [math]n - 1[/math] листьями, количество внутренних вершин которого на [math]1[/math] меньше, чем в исходном дереве. Тогда по индукционному предположению у него менее [math]n - 1[/math] внутренних вершин, значит, в исходном дереве их меньше [math]n[/math].
[math]\triangleleft[/math]

Занимаемая память

Представим дерево как двумерный массив размера [math]|V| \times |\Sigma|[/math], где [math]|V|[/math] — количество вершин в дереве, [math]|\Sigma|[/math] — мощность алфавита. Для любого суффиксного дерева верна предыдущая лемма (у каждой вершины, по определению, не менее двух детей), значит, [math]|V| = O(2 n)[/math]. Каждая [math][i][j][/math] ячейка содержит информацию о том, в какую вершину ведет ребро из [math]i[/math]-ой вершины по [math]j[/math]-ому символу и индексы [math]l, r[/math] начала и конца подстроки, записанной на данном переходе. Итак, дерево занимает [math]O(n|\Sigma|)[/math] памяти.

Построение суффиксного дерева

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

Рассмотрим наивный алгоритм построения суффиксного дерева строки [math]s[/math]:

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 //если мы не можем пойти из вершины по символу [math] l [/math]
            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


Этот алгоритм работает за время [math]O(n^2)[/math], однако алгоритм Укконена позволяет построить сжатое суффиксное дерево за [math]O(n)[/math].

Построение из суффиксного массива

Пусть нам известен суффиксный массив [math]suf[/math] строки [math]s[/math], его можно получить алгоритмом Карккайнена-Сандерса за линейное время. Для преобразования нам также понадобится массив [math]lcp[/math] (longest common prefix), который можно получить алгоритмом Касаи.

В этом преобразовании используется тот же инвариант, что и в других суффиксных структурах:

  1. Строка [math]s[/math] заканчивается специальным символом, который больше не встречается в строке.
  2. (Следствие) [math]lcp[i] \lt len[i - 1][/math], где [math]len[i - 1][/math] — длина суффикса, соответствующего [math]suf[i - 1][/math].

В вершинах дерева мы будем хранить предка [math]parent[/math], стек детей в лексикографическом порядке ребер [math]children[/math], глубину вершины в символах от корня [math]depth[/math].

Будем добавлять суффиксы в порядке, заданном суффиксным массивом [math]suf[/math]. Также будем запоминать последнюю добавленную вершину [math]previous[/math]. Тогда каждый [math]i[/math]-ый добавленный суффикс будет иметь с предыдущим [math]lcp[i][/math] общих символов, что и будет использоваться для быстрого добавления.

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

В процессе построения мы нигде не запоминали сами позиции строки, соответствующие ребрам. Чтобы их восстановить, достаточно определить максимальный суффикс, который проходит по этому ребру. Для этого с помощью обхода в глубину посчитаем для каждой вершину дерева максимальную глубину ее листа [math]maxDepth[/math].

Тогда ребро [math]s[start, end][/math] определяется так:

calculatePositions(Node parent, Node child, int stringLength)
   start = stringLength - child.maxDepth + parent.depth
   end = start + child.depth - parent.depth - 1

Для асимптотического анализа будем использовать в качестве потенциала глубину в вершинах. При добавлении суффикса мы спускаемся один раз, подняться выше корня мы не можем, значит и подниматься мы будем суммарно [math]O(n)[/math] раз. Обход в глубину также выполняется за [math]O(n)[/math], итоговая асимптотика [math]O(n)[/math].

Таким образом, мы умеем за [math]O(n)[/math] строить суффиксное дерево, суффиксный массив и преобразовывать одно в другое. Это говорит об эквивалентности этих структур, и мы можем выбирать нужное представление исходя из требуемых свойств.

Использование сжатого суффиксного дерева

Суффиксное дерево позволяет за линейное время найти:

  • Количество различных подстрок данной строки
  • Наибольшую общую подстроку двух строк
  • Суффиксный массив и массив [math]lcp[/math] (longest common prefix) исходной строки

Источники

  • Дэн ГасфилдСтроки, деревья и последовательности в алгоритмах: Информатика и вычислительная биология — СПб.: Невский Диалект; БХВ-Петербург, 2003. — 654 с: ил.

См. также