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

Материал из Викиконспекты
Перейти к: навигация, поиск
(Построение суффиксного дерева)
(pseudocode correction and some other small fixes)
Строка 1: Строка 1:
[[Суффиксный бор|Суффиксный бор]] {{---}} удобная структура данных для поиска подстроки в строке, но она занимает много места в памяти. Рассмотрим в боре все пути от <tex>u</tex> до  <tex>v</tex>, в которых у каждой вершины только один сын. Такой путь можно сжать до ребра <tex>u v</tex>, записав на нем все встречающиеся на пути символы, которые являются подстрокой исходной строки. Для хранения ее на ребре обычно используют индексы <tex>l, r</tex> начала и конца. Получилось '''сжатое суффиксное дерево'''.
+
[[Суффиксный бор|Суффиксный бор]] {{---}} удобная структура данных для поиска подстроки в строке, но она требует порядка квадрата длины исходной строки памяти. Оптимизацией суффиксного бора, требующей линейное количество памяти, является '''сжатое суффиксное дерево''' рассматриваемое далее.
  
 
==Определение==
 
==Определение==
Строка 12: Строка 12:
  
 
[[Файл:Suffix_tree_3.png|thumb|right|Суффиксное дерево для строки <tex>xabxa</tex> с защитным символом]]  
 
[[Файл:Suffix_tree_3.png|thumb|right|Суффиксное дерево для строки <tex>xabxa</tex> с защитным символом]]  
Рассмотрим дерево для строки <tex>xabxa</tex>. У него суффикс <tex>xa</tex> является префиксом суффикса <tex>xabxa</tex>, значит, этот суффикс не закачивается в листе. Для решения проблемы в конце строки <tex>s</tex> добавляют символ, не входящий в исходный алфавит: '''''защитный''''' символ. Как правило, это <tex>\$</tex>. Любой суффикс строки с защитным символом действительно заканчивается в листе и только в листе.
+
'''Данное определение порождает следующую проблему:''' <br/>
 +
Рассмотрим дерево для строки <tex>xabxa</tex>: суффикс <tex>xa</tex> является префиксом суффикса <tex>xabxa</tex>, а, значит, этот суффикс не закачивается в листе. Для решения проблемы в конце строки <tex>s</tex> добавляют символ, не входящий в исходный алфавит: '''''защитный''''' символ. Обозначим его как <tex>\$</tex>. Любой суффикс строки с защитным символом действительно заканчивается в листе и только в листе, т. к. в такой строке не существует двух различных подстрок одинаковой длины, заканчивающихся на <tex>\$</tex>.
  
Далее <tex>n</tex> - длина строки <tex>s</tex> с защитным символом.
+
Далее <tex>n</tex> {{---}} длина строки <tex>s</tex> с защитным символом.
  
 
==Количество вершин==
 
==Количество вершин==
По определению, в суффиксном дереве содержится <tex>n</tex> листьев. Рассмотрим количество внутренних вершин такого дерева.
+
По определению, в суффиксном дереве содержится <tex>n</tex> листьев. Оценим количество внутренних вершин такого дерева.
  
 
{{Лемма
 
{{Лемма
Строка 27: Строка 28:
 
'''База'''
 
'''База'''
  
При <tex>n = 2</tex> в дереве одна внутренняя вершина - верно.
+
При <tex>n = 2</tex> в дереве одна внутренняя вершина, следовательно утверждение верно.
  
 
'''Переход''' <tex>n \rightarrow n + 1</tex>
 
'''Переход''' <tex>n \rightarrow n + 1</tex>
  
Возьмем вершину в дереве с <tex>n + 1</tex> листами, у которой два ребенка - листья. Рассмотрим возможные случаи:
+
Возьмем вершину в дереве с <tex>n + 1</tex> листами, у которой два ребенка {{---}} листья. Рассмотрим возможные случаи:
  
1) У нее более двух детей. Тогда отрежем от нее лист. Получим дерево с <tex>n</tex> листьями, причем в нем количество внутренних вершин такое же, как в исходном дереве. Но у полученного дерева по индукционному предположению менее <tex>n</tex> внутренних вершин, значит, для исходного дерева лемма верна.
+
1) У нее более двух детей. Тогда отрежем от нее лист. Получим дерево с <tex>n</tex> листьями, причем в нем количество внутренних вершин такое же, как в исходном дереве. Но у полученного дерева по индукционному предположению менее <tex>n</tex> внутренних вершин, а, значит, и для исходного дерева лемма верна.
  
2) У нее ровно два ребенка. Отрежем их, получим дерево с <tex>n</tex> листьями, количество внутренних вершин которого на <tex>1</tex> меньше, чем в исходном дереве. Тогда по индукционному предположению у него менее <tex>n</tex> внутренних вершин, значит, в исходном дереве их меньше <tex>n + 1</tex>.
+
2) У нее ровно два ребенка. Отрежем их, получим дерево с <tex>n - 1</tex> листьями, количество внутренних вершин которого на <tex>1</tex> меньше, чем в исходном дереве. Тогда по индукционному предположению у него менее <tex>n - 1</tex> внутренних вершин, значит, в исходном дереве их меньше <tex>n</tex>.
 
}}
 
}}
  
 
==Занимаемая память==
 
==Занимаемая память==
Представим дерево как массив <tex>[|V|*|\Sigma|]</tex>, где <tex>|V|</tex> {{---}} количество вершин в дереве, <tex>|\Sigma|</tex> - мощность алфавита. Для любого суффиксного дерева верна предыдущая лемма (у каждой вершины по определению не менее двух детей), значит, <tex>|V| = O(2*n)</tex>. Каждая <tex>[i][j]</tex> ячейка содержит информацию о том, в какую вершину ведет <tex>i-</tex>ое ребро по <tex>j-</tex>ому символу и индексы <tex>l, r</tex>. Итак, дерево занимает <tex>O(n*|\Sigma|)</tex> памяти.
+
Представим дерево как двумерный массив размера <tex>|V| \times |\Sigma|</tex>, где <tex>|V|</tex> {{---}} количество вершин в дереве, <tex>|\Sigma|</tex> {{---}} мощность алфавита. Для любого суффиксного дерева верна предыдущая лемма (у каждой вершины, по определению, не менее двух детей), значит, <tex>|V| = O(2 n)</tex>. Каждая <tex>[i][j]</tex> ячейка содержит информацию о том, в какую вершину ведет ребро из <tex>i</tex>-ой вершины по <tex>j</tex>-ому символу и индексы <tex>l, r</tex> начала и конца подстроки, записанной на данном переходе. Итак, дерево занимает <tex>O(n|\Sigma|)</tex> памяти.
  
 
==Построение суффиксного дерева==
 
==Построение суффиксного дерева==
Рассмотрим наивный алгоритм построения суффиксного дерева:
+
Рассмотрим наивный алгоритм построения суффиксного дерева строки <tex>s</tex>:
<tex>count \leftarrow 0</tex> //номер последней вершины, созданной в дереве (глобальная переменная)
+
go[0] = new Vertex() //корень
  '''for''' <tex> i \leftarrow 0 </tex> '''to''' <tex> n </tex> '''do''' //для каждого символа строки
+
count = 0 //номер последней вершины, созданной в дереве (глобальная переменная)
     insert(<tex>i, n</tex>) //добавляем суффикс, начинающийся с него
+
  '''for''' i = 0 '''to''' n //для каждого символа строки
 +
     insert(i, n) //добавляем суффикс, начинающийся с него
  
 
  insert(l, r)
 
  insert(l, r)
     <tex> cur \leftarrow root </tex>
+
     cur = 0
     '''while''' (<tex> l < r </tex>)
+
     '''while''' (l < r)
          '''if''' <tex> go[cur][s[l]].v = 0 </tex> //если мы не можем пойти из вершины по символу <tex> l </tex>
+
        '''if''' go[cur][s[l]].v == -1  '''then''' //если мы не можем пойти из вершины по символу <tex> l </tex>
              createVertex(<tex>cur, l, r</tex>) //создаем новую вершину  
+
            createVertex(cur, l, r) //создаем новую вершину  
          '''else'''
+
        '''else'''
              <tex>start \leftarrow go[cur][s[l]].l </tex>
+
            start = go[cur][s[l]].l
              <tex>finish \leftarrow go[cur][s[l]].r </tex>
+
            finish = go[cur][s[l]].r
              <tex>hasCut \leftarrow false </tex>
+
            hasCut = false
              '''for''' <tex> j = start </tex> '''to''' <tex> finish </tex> //для каждого символа на ребре из текущей вершины
+
            '''for''' j = start '''to''' finish //для каждого символа на ребре из текущей вершины
                    '''if''' <tex>s[l+j-start] <>s[j] </tex> //если нашли не совпадающий символ
+
                '''if''' s[l+j-start] <> s[j] '''then''' //если нашли не совпадающий символ
                        '''cutEdge()''' //создаем вершину на ребре
+
                    //создаем вершину на ребре
                        <tex>hasCut \leftarrow true </tex>
+
                    old = go[cur][s[l]]
                        '''break'''
+
                    createVertex(cur, l, j - 1)
              '''if''' <tex>!hasCut</tex>
+
                    go[count][s[j]].v = old
                    <tex>cur \leftarrow go[cur][s[l]].v </tex> //переходим по ребру
+
                    go[count][s[j]].r = j
                    <tex>l \leftarrow l + finish - start </tex> //двигаемся по суффиксу на длину подстроки, записанной на ребре
+
                    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(<tex>cur, l, r</tex>)
+
  createVertex(cur, l, r)
     <tex>go[cur][s[l]] \leftarrow new Vertex()</tex>
+
     go[++count] = new Vertex()
     <tex>go[cur][s[l]].v \leftarrow count</tex>
+
     go[cur][s[l]].v = count
     <tex>go[cur][s[l]].l \leftarrow l</tex>
+
     go[cur][s[l]].l = l
     <tex>go[cur][s[l]].r \leftarrow r</tex>
+
     go[cur][s[l]].r = r
    <tex>count++</tex>
 
  
  

Версия 14:47, 12 июня 2012

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

Определение

Определение:
Суффиксное дерево (сжатое суффиксное дерево) [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]lcp[/math] (longest common prefix) исходной строки

Источники

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

См. также