Сжатое суффиксное дерево — различия между версиями
| Shersh (обсуждение | вклад) м (псевдо-правки) | Genyaz (обсуждение | вклад)   (→Построение из суффиксного массива) | ||
| Строка 92: | Строка 92: | ||
| # Строка <tex>s</tex> заканчивается специальным символом, который больше не встречается в строке. | # Строка <tex>s</tex> заканчивается специальным символом, который больше не встречается в строке. | ||
| # (Следствие) <tex>lcp[i] < len[i - 1]</tex>, где <tex>len[i - 1]</tex> {{---}} длина суффикса, соответствующего <tex>suf[i - 1]</tex>. | # (Следствие) <tex>lcp[i] < len[i - 1]</tex>, где <tex>len[i - 1]</tex> {{---}} длина суффикса, соответствующего <tex>suf[i - 1]</tex>. | ||
| + | |||
| + | Будем строить дерево, добавляя суффиксы в лексикографическом порядке. Чтобы ускорить добавление, будем использовать то, что <tex>i</tex>-ый суффикс имеет с предыдущим <tex>lcp[i]</tex> общих символов. Тогда добавление из корня не отличается от того, что мы поднимемся вверх из предыдущего суффикса до глубины <tex>lcp[i]</tex> и продолжим построение оттуда. Инвариант позволяет нам утверждать, что ни один лист мы не сможем продолжить, и нам всегда нужно будет хоть раз подняться из него вверх. Поскольку суффиксы отсортированы лексикографически, мы не будем спускаться по ребру после того, как уже поднялись из него из-за несовпадения символа. Все это позволяет сформулировать алгоритм добавления суффикса по известной вершине предыдущего суффикса: | ||
| + | # Подняться из вершины вверх до глубины <tex>lcp</tex> | ||
| + | # Если эта глубина находится на ребре, разрезать ребро по ней. | ||
| + | # Вставить новую вершину как сына вершины с глубиной <tex>lcp</tex> | ||
| В вершинах дерева <tex>Node</tex> мы будем хранить предка <tex>parent</tex>, [[Стек| стек]] детей в лексикографическом порядке ребер <tex>children</tex>, глубину вершины в символах от корня <tex>depth</tex>.   | В вершинах дерева <tex>Node</tex> мы будем хранить предка <tex>parent</tex>, [[Стек| стек]] детей в лексикографическом порядке ребер <tex>children</tex>, глубину вершины в символах от корня <tex>depth</tex>.   | ||
| Соответственно, конструктор вершины имеет вид <code>Node(Node parent, '''int''' depth)</code>. | Соответственно, конструктор вершины имеет вид <code>Node(Node parent, '''int''' depth)</code>. | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| <code> | <code> | ||
Версия 17:21, 1 мая 2014
Суффиксный бор — удобная структура данных для поиска подстроки в строке, но она требует порядка квадрата длины исходной строки памяти. Оптимизацией суффиксного бора, требующей линейное количество памяти, является сжатое суффиксное дерево рассматриваемое далее.
Содержание
Определение
| Определение: | 
| Суффиксное дерево (сжатое суффиксное дерево)  для строки  (где  ) — дерево с  листьями, обладающее следующими свойствами: 
 | 
Данное определение порождает следующую проблему: 
Рассмотрим дерево для строки : суффикс  является префиксом суффикса , а, значит, этот суффикс не закачивается в листе. Для решения проблемы в конце строки  добавляют символ, не входящий в исходный алфавит: защитный символ. Обозначим его как . Любой суффикс строки с защитным символом действительно заканчивается в листе и только в листе, т. к. в такой строке не существует двух различных подстрок одинаковой длины, заканчивающихся на .
Далее — длина строки с защитным символом.
Количество вершин
По определению, в суффиксном дереве содержится листьев. Оценим количество внутренних вершин такого дерева.
| Лемма: | 
| Количество внутренних вершин дерева, каждая из которых имеет не менее двух детей, меньше количества листьев. | 
| Доказательство: | 
| Докажем лемму индукцией по количеству листьев . База При в дереве одна внутренняя вершина, следовательно утверждение верно. Переход Возьмем вершину в дереве с листами, у которой два ребенка — листья. Рассмотрим возможные случаи: 1) У нее более двух детей. Тогда отрежем от нее лист. Получим дерево с листьями, причем в нем количество внутренних вершин такое же, как в исходном дереве. Но у полученного дерева по индукционному предположению менее внутренних вершин, а, значит, и для исходного дерева лемма верна.2) У нее ровно два ребенка. Отрежем их, получим дерево с листьями, количество внутренних вершин которого на меньше, чем в исходном дереве. Тогда по индукционному предположению у него менее внутренних вершин, значит, в исходном дереве их меньше . | 
Занимаемая память
Представим дерево как двумерный массив размера , где — количество вершин в дереве, — мощность алфавита. Для любого суффиксного дерева верна предыдущая лемма (у каждой вершины, по определению, не менее двух детей), значит, . Каждая ячейка содержит информацию о том, в какую вершину ведет ребро из -ой вершины по -ому символу и индексы начала и конца подстроки, записанной на данном переходе. Итак, дерево занимает памяти.
Построение суффиксного дерева
Наивный алгоритм
Рассмотрим наивный алгоритм построения суффиксного дерева строки :
go[0] = 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:       // если мы не можем пойти из вершины по символу 
            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]: // если нашли не совпадающий символ
                    // создаем вершину на ребре
                    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:
                cur = go[cur][s[l]].v  // переходим по ребру
                l = l + finish - start // двигаемся по суффиксу на длину подстроки, записанной на ребре
            else:
                break
createVertex(cur, l, r)
    go[++count] = 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
В процессе построения мы нигде не запоминали сами позиции строки, соответствующие ребрам. Чтобы их восстановить, достаточно определить максимальный суффикс, который проходит по этому ребру. Для этого с помощью обхода в глубину посчитаем для каждой вершину дерева максимальную глубину ее листа .
Тогда ребро определяется так:
function calculatePositions(Node parent, Node child, int stringLength): start = stringLength - child.maxDepth + parent.depth end = start + child.depth - parent.depth - 1
Для асимптотического анализа будем использовать в качестве потенциала глубину в вершинах. При добавлении суффикса мы спускаемся один раз, подняться выше корня мы не можем, значит и подниматься мы будем суммарно раз. Обход в глубину также выполняется за , итоговая асимптотика .
Использование сжатого суффиксного дерева
Суффиксное дерево позволяет за линейное время найти:
- Количество различных подстрок данной строки
- Наибольшую общую подстроку двух строк
- Суффиксный массив и массив (longest common prefix) исходной строки
Построение суффиксного массива и массива lcp из суффиксного дерева
Пусть к строке дописан специальный символ для сохранения инварианта. Рассмотрим лексикографический по ребрам порядок обхода сжатого суффиксного дерева. Пусть два суффикса имеют общее начало, но различаются в -ом символе. Первым будет рассмотрено поддерево по ребру с меньшим символом, значит и лист, соответствующий этому суффиксу, будет посещен первым.
Тогда суффиксный массив строится из суффиксного дерева обходом в глубину в указанном порядке. Пусть длина строки , глубина листа в символах , тогда номер суффикса .
Для заполнения массива нам понадобится вершина , которая будет означать вершину с минимальной глубиной, в которую мы поднимались при переходе между суффиксами. Поскольку мы точно поднимались туда, но не поднимались выше, это будет наименьший общий предок этих узлов. Из этого следует, что у рассматриваемых суффиксов совпадает ровно символов.
int curPos = 0
Node minNode = root
// Для заполнения нужно вызвать dfs(root) 
function dfs(Node n):
   if children.size == 0:
      suf[curPos] = length - n.depth
      lcp[curPos] = minNode.depth
      curPos++
      minNode = n
   else:
      foreach child in n.children:
         if n.depth < minNode.depth:
            minNode = n
         dfs(child)
Асимптотика алгоритма совпадает с асимптотикой обхода в глубину и составляет .
Таким образом, мы умеем за строить суффиксное дерево, суффиксный массив и преобразовывать одно в другое.
Источники
- Дэн Гасфилд — Строки, деревья и последовательности в алгоритмах: Информатика и вычислительная биология — СПб.: Невский Диалект; БХВ-Петербург, 2003. — 654 с: ил.

