Алгоритм МакКрейта — различия между версиями
| Genyaz (обсуждение | вклад) м | Genyaz (обсуждение | вклад)   (Добавлен псевдокод) | ||
| Строка 47: | Строка 47: | ||
| Покажем, что это невозможно. Рассмотрим, что значит, что <tex>head_{i-1}.suf</tex> остановилась посередине ребра. Это означает, что все суффиксы <tex>s[j..n], j < i - 1</tex>, которые дошли до этого места, имеют совпадающие следующие символы, по определению <tex>head_{i-1}</tex> отличающиеся от символа суффикса <tex>s[i - 1..n]</tex>. Тогда и <tex>s[i..n]</tex> должен отличаться в этом символе, значит <tex>head_i = head_{i-1}.suf</tex>. | Покажем, что это невозможно. Рассмотрим, что значит, что <tex>head_{i-1}.suf</tex> остановилась посередине ребра. Это означает, что все суффиксы <tex>s[j..n], j < i - 1</tex>, которые дошли до этого места, имеют совпадающие следующие символы, по определению <tex>head_{i-1}</tex> отличающиеся от символа суффикса <tex>s[i - 1..n]</tex>. Тогда и <tex>s[i..n]</tex> должен отличаться в этом символе, значит <tex>head_i = head_{i-1}.suf</tex>. | ||
| }} | }} | ||
| + | |||
| + | == Псевдокод == | ||
| + | В вершинах дерева <tex>Node</tex> будем хранить следующую информацию: | ||
| + | * <tex>parent</tex> - предок; | ||
| + | * <tex>start, end</tex> - позиции строки, соответствующие ребру до предка; | ||
| + | * <tex>length</tex> - длина ребра до предка | ||
| + | * <tex>depth</tex> - глубина вершины в символах; | ||
| + | * <tex>suf</tex> - суффиксная ссылка; | ||
| + | * <tex>children[]</tex> - массив детей. | ||
| + | |||
| + | Конструктор будет иметь вид <code>Node(Node parent, '''int''' start, '''int''' end, '''int''' depth)</code> | ||
| + | |||
| + | <code> | ||
| + |  '''string''' s | ||
| + |  '''int''' n = s.length | ||
| + | |||
| + |  Node buildSuffixTree() | ||
| + |     superRoot = Node('''null''', 0, -1, 0) | ||
| + |     root = Node(superRoot, 0, -1, 0) | ||
| + |     root.suf = superRoot | ||
| + |     '''for''' c '''in''' <tex>\Sigma</tex> | ||
| + |        superRoot.children[c] = root | ||
| + |     head = root  | ||
| + |     '''for''' i = 1 '''to''' n | ||
| + |        head = addSuffix(head, i) | ||
| + |     '''return''' root | ||
| + | |||
| + |  Node addSuffix(Node head, '''int''' start) | ||
| + |     newHead = slowScan(fastScan(head), start) | ||
| + |     newLeaf = Node(newHead, start + newHead.depth, n, n - start + 1) | ||
| + |     newHead.children[s[start + newHead.depth]] = newLeaf | ||
| + |     '''return''' newHead | ||
| + | |||
| + |  Node fastScan(Node head) | ||
| + |     '''if''' head.suf == '''null''' | ||
| + |        skipped = head.length | ||
| + |        curPos = head.start | ||
| + |        curNode = head.parent.suf | ||
| + |        '''while''' curNode.children[s[curPos]].length >= skipped | ||
| + |           curNode = curNode.children[s[curPos]]  | ||
| + |           skipped -= curNode.length | ||
| + |           curPos += curNode.length | ||
| + |        '''if''' skipped > 0 | ||
| + |           newNode = split(curNode, curNode.children[s[curPos]], skipped) | ||
| + |           head.suf = newNode | ||
| + |     '''return''' head.suf | ||
| + | |||
| + |  Node split(Node parent, Node child, '''int''' edgeLength) | ||
| + |     newNode = Node(parent, child.start, child.start + edgeLenth - 1, parent.depth + edgeLength) | ||
| + |     parent.children[s[child.start]] = newNode | ||
| + |     child.start += edgeLength | ||
| + |     child.length -= edgeLength  | ||
| + |     newNode.children[s[child.start]] = child | ||
| + |     child.parent = newNode | ||
| + |     '''return''' newNode | ||
| + | |||
| + |  Node slowScan(Node node, '''int''' start) | ||
| + |     curNode = node | ||
| + |     curPos = start + node.depth | ||
| + |     '''while''' ''true'' | ||
| + |        '''if''' curNode.children[s[curPos]] == '''null''' | ||
| + |           '''break'''  | ||
| + |        child = curNode.children[s[curPos]] | ||
| + |        edgePos = 0 | ||
| + |        '''while''' child.start + edgePos <= child.end '''and''' s[child.start + edgePos] == s[curPos]     | ||
| + |           curPos++ | ||
| + |           edgePos++ | ||
| + |        '''if''' child.start + edgePos > child.end | ||
| + |           curNode = child | ||
| + |        '''else''' | ||
| + |           curNode = split(curNode, child, edgePos) | ||
| + |           '''break'''           | ||
| + |     '''return''' curNode | ||
| + | </code> | ||
| == Асимптотическая оценка == | == Асимптотическая оценка == | ||
Версия 16:05, 5 мая 2014
Алгоритм МакКрейта — алгоритм построения суффиксного дерева для заданной строки за линейное время. Отличается от алгоритма Укконена тем, что добавляет суффиксы в порядке убывания длины.
Содержание
Историческая справка
Первым оптимальным по времени был алгоритм, предложенный Вайнером в 1973 году. Идея алгоритма была в нахождении первых символов суффикса, которые находились в уже построенном дереве. Суффиксы просматривались от самого короткого к самому длинному, а для быстрого поиска использовались по два массива размера алфавита на каждую вершину, что затрудняло как понимание алгоритма, так и его реализацию и эффективность, особенно в плане занимаемой памяти. МакКрейт в 1976 году предложил свой алгоритм, в котором порядок добавления суффиксов заменен на обратный, а для быстрого вычисления места, откуда нужно продолжить построение нового суффикса, достаточно суффиксной ссылки в каждой вершине. В 1995 году Укконен представил свою версию алгоритма, которая считается наиболее простой для понимания, а также, в отличие от алгоритмов Вейнера и МакКрейта, является online алгоритмом, способным строить неявное суффиксное дерево по мере прочтения строки, а затем превратить его в настоящее.
Теоретическое обоснование
Рассмотрим строку длины , которая заканчивается специальным символом, не встречающимся больше в строке. Заметим, что если два суффикса имеют (largest common prefix) общих символов, то в построенном суффиксном дереве они будут иметь наименьшего общего предка на этой глубине. Будем рассматривать суффиксы в порядке убывания длины, тогда имеет смысл узнавать наибольшее с новым суффиксом среди всех суффиксов, добавленных раньше. Обозначим как — максимальный префикс и среди всех .
Пусть мы знаем и место в дереве, которое ему соответствует. Если позиция находится на ребре, разрежем его, а потом добавим новую вершину. Считать по определению было бы очень затруднительно, но существует способ значительно сократить вычисления.
| Лемма: | 
| Пусть , тогда  — префикс . | 
| Доказательство: | 
| 
 | 
Если нам известны суффиксные ссылки для каждой вершины , мы можем быстро перейти от позиции к ее суффиксу и продолжить сравнение символов оттуда. Если бы новая позиция всегда оказывалась существующей вершиной построенного дерева, этот алгоритм бы уже работал, но в реальности можно оказаться на середине ребра, для которой суффиксная ссылка неизвестна. Для нахождения ее суффиксной ссылки на следующей итерации мы сначала перейдем к предку, пройдем по суффиксной ссылке, а уже затем будем продолжать сравнение.
Алгоритм
Для удобства реализации вместе с корнем создадим вспомогательную вершину , обладающую свойствами:
- Для любого символа из вершины есть ребро в .
Будем поддерживать инвариант:
- Для всех вершин, кроме, возможно, последней добавленной , известны суффиксные ссылки.
- Суффиксная ссылка всегда ведет в вершину, а не в середину ребра.
При добавлении каждого следующего суффикса будем выполнять следующие шаги:
-  Если суффиксная ссылка  не определена:
- Поднимемся вверх к ее предку;
- Пройдем по суффиксной ссылке;
- Спустимся вниз на столько символов, сколько мы прошли вверх к предку (fast scanning).
- Если мы оказались посередине ребра, разрежем его и добавим вершину.
- Установим суффиксную ссылку для
 
- Иначе просто пройдем по суффиксной ссылке.
- Будем идти по дереву вниз, пока либо не будет перехода по символу, либо очередной символ на ребре не совпадет с символом нового суффикса (slow scanning)
- Добавим ребро/разрежем существующее, запомним новую позицию и добавим оставшуюся часть суффикса в качестве листа.
| Утверждение: | 
| Инвариант алгоритма сохраняется | 
| Инвариант мог бы нарушиться только в случае, если бы не существовало вершины в суффиксной ссылке для , но мы продолжили бы сканирование по ребру дальше и получили две вершины с неопределенными суффиксными ссылками.Покажем, что это невозможно. Рассмотрим, что значит, что остановилась посередине ребра. Это означает, что все суффиксы , которые дошли до этого места, имеют совпадающие следующие символы, по определению отличающиеся от символа суффикса . Тогда и должен отличаться в этом символе, значит . | 
Псевдокод
В вершинах дерева будем хранить следующую информацию:
- - предок;
- - позиции строки, соответствующие ребру до предка;
- - длина ребра до предка
- - глубина вершины в символах;
- - суффиксная ссылка;
- - массив детей.
Конструктор будет иметь вид Node(Node parent, int start, int end, int depth)
string s int n = s.length
Node buildSuffixTree()
   superRoot = Node(null, 0, -1, 0)
   root = Node(superRoot, 0, -1, 0)
   root.suf = superRoot
   for c in 
      superRoot.children[c] = root
   head = root 
   for i = 1 to n
      head = addSuffix(head, i)
   return root
Node addSuffix(Node head, int start) newHead = slowScan(fastScan(head), start) newLeaf = Node(newHead, start + newHead.depth, n, n - start + 1) newHead.children[s[start + newHead.depth]] = newLeaf return newHead
Node fastScan(Node head)
   if head.suf == null
      skipped = head.length
      curPos = head.start
      curNode = head.parent.suf
      while curNode.children[s[curPos]].length >= skipped
         curNode = curNode.children[s[curPos]] 
         skipped -= curNode.length
         curPos += curNode.length
      if skipped > 0
         newNode = split(curNode, curNode.children[s[curPos]], skipped)
         head.suf = newNode
   return head.suf
Node split(Node parent, Node child, int edgeLength) newNode = Node(parent, child.start, child.start + edgeLenth - 1, parent.depth + edgeLength) parent.children[s[child.start]] = newNode child.start += edgeLength child.length -= edgeLength newNode.children[s[child.start]] = child child.parent = newNode return newNode
Node slowScan(Node node, int start)
   curNode = node
   curPos = start + node.depth
   while true
      if curNode.children[s[curPos]] == null
         break 
      child = curNode.children[s[curPos]]
      edgePos = 0
      while child.start + edgePos <= child.end and s[child.start + edgePos] == s[curPos]    
         curPos++
         edgePos++
      if child.start + edgePos > child.end
         curNode = child
      else
         curNode = split(curNode, child, edgePos)
         break          
   return curNode
Асимптотическая оценка
В приведенном алгоритме используется константное число операций на добавление одного суффикса, не считая slow scanning и fast scanning.
Slow scanning делает операций, что суммарно дает операций.
Fast scanning работает с целыми ребрами, поэтому будем использовать в качестве потенциала глубину в вершинах. Из структуры суффиксного дерева мы знаем, что суффиксная ссылка может уменьшить глубину вершины не более, чем на , так что мы на каждой итерации поднимаемся не более, чем на — один раз к предку, а потом по суффиксной ссылке, что составляет за весь алгоритм. Соответственно, спустимся мы тоже суммарно раз, так как и максимальная глубина составляет .
Итоговая асимптотика алгоритма — .
Сравнение с другими алгоритмами
В сравнении с алгоритмом Вайнера:
- Преимущества: каждая вершина хранит только суффиксную ссылку, а не массивы размера алфавита.
- Недостатки: нет.
В сравнении с алгоритмом Укконена:
- Преимущества: мы строим суффиксное дерево в явной форме, что может облегчить понимание алгоритма.
- Недостатки: является offline алгоритмом, то есть требует для начала работы всю строку целиком.
Источники
- Suffix tree - Wikipedia
- Gusfield, Dan , Algorithms on Strings, Trees and Sequences: Computer Science and Computational Biology // Cambridge University Press, — 1999. — ISBN: 0-521-58519-8
- C. N. Storm, McCreight's suffix tree construction algorithm
