Опубликован: 08.04.2009 | Доступ: свободный | Студентов: 485 / 0 | Длительность: 17:26:00
Специальности: Программист
Лекция 10:

Сопоставление с образцом

10.8. Суффиксные деревья

До сих про наши программы сначала получали образец, который надо искать, а потом текст, в котором надо искать. В следующих задачах все наоборот.

10.8.1. Программа получает на вход слово Y длины m и может его обрабатывать (пока без ограничений на время и память). Затем она получает слово X длины n и должна сообщить, является ли оно подсловом слова Y. При этом число операций при обработке слова X должно быть порядка n (не превосходить cn, где константа c может зависеть от размера алфавита). Как написать такую программу?

Решение. Пока не накладывается никаких ограничений на время и память при обработке Y, это не представляет труда. Именно, надо склеить все подслова слова Y в дерево, объединив слова с общими началами (как мы это делали, распознавая вхождения нескольких образцов). Например, для Y=ababc получится такое дерево подслов (на ребре написана буква, которая добавляется при движении по этому ребру; вершины находятся во взаимно однозначном соответствии с подсловами слова Y ):


Пусть такое дерево построено. После этого, читая слово X слева направо, мы прослеживаем X в дереве, начав с корня; слово X будет подсловом слова Y, если при этом мы не выйдем за пределы дерева.

Заметим, что аналогичная конструкция годится для любого множества слов U, а не только для множества всех подслов данного слова: после того как соответствующее дерево построено, мы можем про любое слово X определить его принадлежность к U за время, пропорциональное длине X. (Надо только дополнительно хранить в вершине дерева информацию, принадлежит ли соответствующее ей слово множеству U или лишь является началом другого слова, принадлежащего U.)

10.8.2. Решить предыдущую задачу с дополнительным ограничением: объем используемой памяти пропорционален длине слова Y.

Решение. Прежний способ не годится: число вершин дерева равно числу подслов слова Y, а у слова длины m число подслов может быть порядка m^2, а не m. Однако мы можем "сжать" наше дерево, оставив вершинами лишь точки ветвления (где больше одного сына). Тогда на ребрах дерева надо написать уже не буквы, а куски слова Y.

Вот что получится при сжатии нашего примера:


Будем считать (здесь и далее), что последняя буква слова Y больше в нем не встречается. (Этого всегда можно достичь, дописав дополнительный фиктивный символ.) Тогда листья сжатого дерева соответствуют концам слова Y, а внутренние вершины (точки ветвления) - таким подсловам s слова Y, которые встречаются в Y несколько раз, и притом с разными буквами после s.

У каждой внутренней вершины (не листа) сжатого дерева есть не менее двух сыновей. В деревьях с такими свойствами число внутренних вершин не превосходит числа листьев. (В самом деле, при движении слева направо в каждой точке ветвления добавляется новый путь к листу.) Поскольку листьев m, всего вершин не более 2m, и мы уложимся в линейную по m память, если будем экономно хранить пометки на ребрах. Каждая такая пометка является подсловом слова Y, и потому достаточно указывать координату ее начала и конца в Y. Это не помешает впоследствии прослеживать произвольное слово X в этом дереве буква за буквой, просто в некоторые моменты мы будем находиться внутри ребер (и должны помнить, внутри какого ребра и в какой позиции мы находимся). При появлении новой буквы слова X ее нужно сравнить с соответствующей буквой пометки этого ребра (что можно сделать за O(1) действий, так как координату этой буквы мы знаем.)

Построенное нами сжатое дерево называют сжатым суффиксным деревом } слова Y (концы слова называют "суффиксами").

10.8.3. Показать, что построение сжатого суффиксного дерева можно выполнить за время O(m^2) с использованием O(m) памяти.

Решение. Будем добавлять в суффиксное дерево суффиксы по очереди. Добавление очередного суффикса делается так же, как и проверка принадлежности: мы читаем его буква за буквой и прокладываем путь в дереве. В некоторый момент добавляемый суффикс выйдет за пределы дерева (напомним, что мы считаем, что последний символ слова уникален).

Если это произойдет посередине ребра, то ребро придется в этом месте разрезать. Ребро превратится в два, его пометка разрежется на две, появится новая вершина (точка ветвления) и ее новый сын-лист. Если точка ветвления совпадет с уже имевшейся в дереве, то у нее появится новый сын-лист. В любом случае после обнаружения места ветвления требуется O(1) операций для перестройки дерева (в частности, разрезание пометки на две выполняется легко, так как пометки хранятся в виде координат начала и конца в слове Y ).

Гораздо более сложной задачей является построение сжатого суффиксного дерева за линейное время (вместо квадратичного, как в предыдущей задаче). Чтобы изложить алгоритм МакКрейта, который решает эту задачу, нам понадобятся некоторые приготовления.

Для начала опишем более подробно структуру дерева, которое мы используем, и операции с ним.

Мы рассматриваем деревья с корнем, на ребрах которых написаны слова (пометки); все пометки являются подсловами некоторого заранее фиксированного слова Y. При этом выполнены такие свойства:

  • каждая внутренняя вершина имеет хотя бы двух сыновей;
  • пометки на ребрах, выходящих из данной вершины, начинаются на разные буквы.

Каждой вершине v такого дерева соответствует слово, которое записано на пути от корня r к вершине v. Будем обозначать это слово s(v). Обозначим пометку на ребре, ведущем к v, через l(v), а отца вершины v - через f(v). Тогда s(r)=\Lambda (пустое слово), а

s(v)=s(f(v))+l(v),
для любой вершины v\ne r (знак " + " обозначает соединение строк).

Помимо вершин дерева, мы будем рассматривать позиции в нем, которые могут быть расположены в вершинах, а также "внутри ребер" (разделяя пометку этого ребра на две части). Формально говоря, позиция представляет собой пару (v,k), где v - вершина (отличная от корня), а k - целое число в промежутке [0, |l(v)|), указывающее, на сколько букв надо вернуться от v к корню. Здесь |l(v)| - длина пометки l(v) ; значение k=l(v) соответствовало бы предыдущей вершине и потому не допускается. К числу позиций мы добавляем также пару (r,0), соответствующую корню дерева. Каждой позиции p=(v,k) соответствует слово s(p), которое получается удалением k последних символов из s(v).

Пусть p - произвольная позиция в дереве, а w - слово. Пройти вдоль w, начиная с p, означает найти другую позицию q, для которой s(q)=s(p)+w. Если такая позиция есть, то (при описанном способе хранения пометок, когда указываются координаты их начала и конца внутри Y ) ее можно найти за время, пропорциональное длине слова w. Если такой позиции нет, то в какой-то момент мы "свернем с пути"; в этот момент можно пополнить дерево, сделав отсутствующую в дереве часть слова w пометкой на пути к новому листу. Надо только, чтобы эта пометка была подсловом слова Y (при нашем способе хранения пометок); это будет гарантировано, если прослеживаемое слово w является подсловом слова Y.

Заметим, что при этом может образоваться новая вершина (если развилка оказалась внутри ребра), а может и не образоваться (если развилка оказалась в вершине). Число действий при такой модификации пропорционально длине пройденной части слова (длина непройденной не важна).

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

10.8.4. Пусть для данной позиции p и слова w заранее известно, что в дереве есть позиция q, для которой s(q)=s(p)+w. Показать, что позицию q можно найти за время, пропорциональное числу ребер дерева на пути от p к q. (Это число может быть значительно меньше длины слова w, если пометки на ребрах длинные.)

Решение. В самом деле, при навигации нужно ориентироваться лишь в вершинах (выбирать исходящее ребро в зависимости от очередной буквы); в остальных местах путь однозначный и потому можно сдвигаться сразу к концу ребра.