Сжатое суффиксное дерево — различия между версиями
Genyaz (обсуждение | вклад) м |
Shersh (обсуждение | вклад) м (псевдо-правки) |
||
| Строка 46: | Строка 46: | ||
===Наивный алгоритм=== | ===Наивный алгоритм=== | ||
Рассмотрим наивный алгоритм построения суффиксного дерева строки <tex>s</tex>: | Рассмотрим наивный алгоритм построения суффиксного дерева строки <tex>s</tex>: | ||
| − | go[0] = | + | go[0] = Vertex() // корень |
| − | count = 0 //номер последней вершины, созданной в дереве (глобальная переменная) | + | count = 0 // номер последней вершины, созданной в дереве (глобальная переменная) |
| − | '''for''' i = 0 '''to''' n //для каждого символа строки | + | '''for''' i = 0 '''to''' n: // для каждого символа строки |
| − | insert(i, n) //добавляем суффикс, начинающийся с него | + | insert(i, n) // добавляем суффикс, начинающийся с него |
| − | insert(l, r) | + | insert(l, r): |
cur = 0 | cur = 0 | ||
| − | '''while''' | + | '''while''' l < r: |
| − | '''if''' go[cur][s[l]].v == -1 | + | '''if''' go[cur][s[l]].v == -1: // если мы не можем пойти из вершины по символу <tex> l </tex> |
| − | createVertex(cur, l, r) //создаем новую вершину | + | createVertex(cur, l, r) // создаем новую вершину |
| − | '''else''' | + | '''else:''' |
start = go[cur][s[l]].l | start = go[cur][s[l]].l | ||
finish = go[cur][s[l]].r | finish = go[cur][s[l]].r | ||
hasCut = false | hasCut = false | ||
| − | '''for''' j = start '''to''' finish //для каждого символа на ребре из текущей вершины | + | '''for''' j = start '''to''' finish: // для каждого символа на ребре из текущей вершины |
| − | '''if''' s[l+j-start] <> s[j] | + | '''if''' s[l+j-start] <tex> \neq </tex> s[j]: // если нашли не совпадающий символ |
| − | //создаем вершину на ребре | + | // создаем вершину на ребре |
old = go[cur][s[l]] | old = go[cur][s[l]] | ||
createVertex(cur, l, j - 1) | createVertex(cur, l, j - 1) | ||
| Строка 71: | Строка 71: | ||
hasCut = true | hasCut = true | ||
'''break''' | '''break''' | ||
| − | '''if''' !hasCut | + | '''if''' !hasCut: |
| − | cur = go[cur][s[l]].v //переходим по ребру | + | cur = go[cur][s[l]].v // переходим по ребру |
| − | l = l + finish - start //двигаемся по суффиксу на длину подстроки, записанной на ребре | + | l = l + finish - start // двигаемся по суффиксу на длину подстроки, записанной на ребре |
| − | '''else''' | + | '''else:''' |
'''break''' | '''break''' | ||
createVertex(cur, l, r) | createVertex(cur, l, r) | ||
| − | go[++count] = | + | go[++count] = Vertex() |
go[cur][s[l]].v = count | go[cur][s[l]].v = count | ||
go[cur][s[l]].l = l | go[cur][s[l]].l = l | ||
| Строка 105: | Строка 105: | ||
<code> | <code> | ||
| − | Node addNextSuffix(Node previous, '''int''' length, '''int''' lcp) | + | Node addNextSuffix(Node previous, '''int''' length, '''int''' lcp): |
| − | '''if''' previous.depth == 0 '''or''' previous.depth == lcp <font color=green> // Добавляем к сыновьям текущей вершины </font> | + | '''if''' previous.depth == 0 '''or''' previous.depth == lcp: <font color=green> // Добавляем к сыновьям текущей вершины </font> |
added = Node(previous, length) | added = Node(previous, length) | ||
previous.children.'''push'''(added) | previous.children.'''push'''(added) | ||
'''return''' added | '''return''' added | ||
| − | '''else''' | + | '''else:''' |
| − | '''if''' previous.parent.depth < lcp <font color=green> // Нужно разрезать ребро </font> | + | '''if''' previous.parent.depth < lcp: <font color=green> // Нужно разрезать ребро </font> |
inserted = Node(prevous.parent, lcp) | inserted = Node(prevous.parent, lcp) | ||
previous.parent.children.'''pop'''() | previous.parent.children.'''pop'''() | ||
| Строка 119: | Строка 119: | ||
'''return''' addNextSuffix(previous.parent, length, lcp) | '''return''' addNextSuffix(previous.parent, length, lcp) | ||
| − | Node buildSuffixTree('''int[]''' suf, '''int[]''' lcp, '''int''' length) | + | Node buildSuffixTree('''int[]''' suf, '''int[]''' lcp, '''int''' length): |
root = Node('''null''', 0) | root = Node('''null''', 0) | ||
previous = root | previous = root | ||
| − | '''for''' i = 1 '''to''' length | + | '''for''' i = 1 '''to''' length: |
previous = addNextSuffix(previous, length - suf[i], lcp[i]) | previous = addNextSuffix(previous, length - suf[i], lcp[i]) | ||
'''return''' root | '''return''' root | ||
| Строка 132: | Строка 132: | ||
<code> | <code> | ||
| − | calculatePositions(Node parent, Node child, '''int''' stringLength) | + | '''function''' calculatePositions(Node parent, Node child, '''int''' stringLength): |
start = stringLength - child.maxDepth + parent.depth | start = stringLength - child.maxDepth + parent.depth | ||
end = start + child.depth - parent.depth - 1 | end = start + child.depth - parent.depth - 1 | ||
| Строка 157: | Строка 157: | ||
<code> | <code> | ||
| − | |||
| − | |||
| − | |||
'''int''' curPos = 0 | '''int''' curPos = 0 | ||
Node minNode = root | Node minNode = root | ||
| − | <font color=green>//Для заполнения нужно вызвать dfs(root) </font> | + | <font color=green>// Для заполнения нужно вызвать dfs(root) </font> |
| − | dfs(Node n) | + | '''function''' dfs(Node n): |
| − | '''if''' children.size == 0 | + | '''if''' children.size == 0: |
suf[curPos] = length - n.depth | suf[curPos] = length - n.depth | ||
lcp[curPos] = minNode.depth | lcp[curPos] = minNode.depth | ||
curPos++ | curPos++ | ||
minNode = n | minNode = n | ||
| − | '''else''' | + | '''else:''' |
| − | '''foreach''' | + | '''foreach''' child '''in''' n.children: |
| − | '''if''' n.depth < minNode.depth | + | '''if''' n.depth < minNode.depth: |
minNode = n | minNode = n | ||
| − | dfs( | + | dfs(child) |
</code> | </code> | ||
Версия 16:13, 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 с: ил.