Алгоритм укконена для обобщенных суффиксных деревьев
В настоящее время я работаю над своей собственной реализацией суффиксного дерева (используя C++, но вопрос остается языковым агностиком). Я изучилоригинальную статью из Укконена . Статья очень ясна, поэтому я приступил к работе над своей реализацией и попытался решить проблему для обобщенных суффиксных деревьев.
В дереве каждая подстрока, ведущая от одного узла к другому, представлена парой целых чисел. Хотя это просто для обычного суффиксного дерева, проблема возникает, когда несколько строк сосуществуют в одном дереве (которое становится обобщенным Суффиксным деревом ). Действительно, теперь такой пары недостаточно, нам нужна другая переменная, чтобы указать, какую строку ссылки мы используем.
Краткий пример. Рассмотрим строку coconut:
- подстрока
nutбудет(4,6). - добавим
troublemakerв дерево,(4,6)Теперь можно:
nutесли мы обратимся к первой строке.
bleЕсли мы обратимся ко второму строка.
Чтобы справиться с этим, я подумал, добавив идентификатор, представляющий строку:
// A pair of int is a substring (regular tree)
typedef std::pair<int,int> substring;
// We need to bind a substring to its reference string:
typedef std::pair<int, substring> mapped_substring;
В настоящее время передо мной стоит следующая проблема:
Я получаю запрос на добавление строки в дерево. Во время работы алгоритма мне, возможно, придется проверять существующие переходы, связанные с другими зарегистрированными строками, представленными в виде триплета (идентификатор ссылочной строки, к, p ). Некоторые операции обновления основаны на индексах подстрок, Как я могу выполнять их в таких условиях ?
Примечание: вопрос является языком-агностиком, поэтому я не включил C++-тег, хотя показан небольшой фрагмент.
2 ответов:
TL; DR
Исходный алгоритм на самом деле не нуждается в модификации, чтобы построить обобщенное дерево суффиксов.Вот github для моей текущей реализации (в C++). Он все еще нуждается в некотором пересмотре и рефакторинге (и некотором обширном тестировании...) но это только начало!
Примечание: В настоящее время я работаю над этой реализацией, я обновлю этот ответ с материалом, когда он будет доступен! (Колиру живет и то любить...)
Детальный анализ
Предчувствие, которое у меня возникло, было верным. Чтобы не отставать от трюков, используемых в оригинальном алгоритме, нам действительно нужно добавить ссылку на исходную строку. Кроме того, алгоритм онлайн , что означает, что вы можете добавлять строки на лету в дерево.Предположим, что у нас естьобобщенное суффиксное дерево GST (N ) для строк (S1, ..., S N). Проблема под рукой вот как обрабатывать процесс построения GST (N+1), используя GST (N).
Настройка модели данных
В простом случае (дерево с одним суффиксом) каждый переход представляет собой пару (подстрок, конечная вершина ). Хитрость алгоритма Укконена заключается в моделировании подстроки с помощью пары указателей на соответствующие позиции в исходной строке. Здесь нам также нужно связать такую подстроку с ее "родительской" строкой . Делать Итак:
- храните исходные строки в хэш-таблице, давая им уникальный целочисленный ключ.
- теперь подстрока становится: (ID, (левый указатель, правый указатель)). Таким образом, мы можем использовать ID для извлечения исходной строки в O(1).
Мы называем это отображенной подстрокой. Я использую c++ typedefs, найденные в моем исходном вопросе:
Как вы увидите, мы также сохраним понятие референтной пары. На этот раз ... опорная пара , как и переход, будет содержать отображенную подстроку.// This is a very basic draft of the Node class used template <typename C> class Node { typedef std::pair<int, int> substring; typedef std::pair<int, substring> mapped_substring; typedef std::pair<mapped_substring, Node*> transition; // C is the character type (basically `char`) std::unordered_map<C, transition> g; // Called g just like in the article :) Node *suffix_link; };Примечание: как и в C++, индексы строк будут начинаться с 0.
Вставка новой строки
Мы хотим вставить S N+1 в GST (N ).
Поступая таким образом, мы заканчиваем в состоянии r. Это состояние может быть явным (т. е. мы закончили прямо на вершине) или неявным (несоответствие произошло в середине перехода). Мы используем ту же концепцию, что и в исходном алгоритме для моделирования такого состояния: a опорная пара . Быстрый пример:
GST (N ) может иметь уже множество узлов и переходов. В простом дереве у нас был бы только корень и специальный узел приемника. Здесь у нас могут быть переходы для S N+1 это уже было добавлено путем вставки некоторых предыдущих строк. Первое, что нужно сделать, это спуститься по дереву через переходы до тех пор, пока оно соответствует S N+1.Когда мы спускаемся по дереву, мы оказываемся в переходе t на символе
- мы хотим вставить S N+1 =
banana- узел s , представляющий
baявно существует в GST (N)- s имеет переход t для подстроки
nall, который является несоответствием. Таким образом, неявное состояние r , которое мы получаем, представлено опорная пара (s, m ), где m - отображенная подстрока (N+1, (1,3)).Здесь r является активной точкой для 5-й итерации алгоритма в построении суффиксного дерева
banana. Тот факт, что мы попали в это состояние, означает именно то, что дерево дляbanaуже построено в GST (N ).В этом примере мы возобновляем алгоритм на 5-й итерации, чтобы построить суффиксное дерево для
banan, используя дерево дляbana. Чтобы не потерять общности, мы будем утверждать, что r = (s, (N+1, (k, i-1)), i - индекс первого несоответствия. Мы действительно и K ≤ я (на равенство-это синоним Р является явное состояние).Свойство: мы можем возобновить алгоритм Укконена для построения GST (N ) на итерации i (вставка символа в индекс i в S N+1). Активная точка для этого итерация - это состояние r , которое мы получаем, спускаясь по дереву. единственная необходимая настройка-это некоторые операции выборки для разрешения подстрок.
Доказательство для свойства
Во-первых, наличие такого состояния r подразумевает, что целые состояния для промежуточного дерева T(N+1) i-1 есть и такие. Итак, все готово, и мы возобновляем алгоритм.Нам нужно доказать, что каждая процедура в алгоритме остается в силе. Существует 3 таких подпрограммы:
test_and_split: учитывая символ, который нужно вставить на текущей итерации, проверьте, нужно ли нам разделить переход на два отдельных перехода, и если текущая точка является конечной точкой.canonize: задана опорная пара (n, m) , где n - вершина, а m - отображенная подстрока, возвращает пару (n', m') , представляющую одно и то же состояние, например m ' - кратчайшее возможные подстроки.update: обновить GST (N ) так, чтобы он имел все состояния для промежуточного дерева T (N+1) i в конце пробега.Test_and_split
Входные данные: вершина s, отображенная подстрока m = (l, (k, p)) и символ t.
вывод: логическое выражение, которое сообщает, Является ли состояние (s, m) конечной точкой для текущей итерации и узлом r явно представляя (s, m) , если это не конечная точка.Простейшем случае идет в первую очередь. Если подстрока пуста (k > p ), то состояние уже представлено явно. Мы просто должны проверить, достигли ли мы конечной точки или нет. в GST, как и в общем суффиксном дереве, есть всегда не более одного перехода на узел, начинающийся с данного символа.Таким образом, если существует переход, начинающийся с t , мы возвращаем true (мы достиг конечной точки), иначе ложь.
Теперь самое сложное, когда k ≤ p. нам сначала нужно получить строку S l лежа в индексе l(*) в исходной таблице строк.
Пусть (l', (k', p')) (ОТВ. s ' ) - подстрока (ОТВ. узел), связанный с переходом TR из s , начинающимся с символа S l (k) (*). Такой переход существует потому, что (s, (l, (k, p)) представляет (существующее) неявное состояние на пограничном пути промежуточного дерева T(N+1) i-1. Кроме того, мы уверены, что первые символы p - k на этом переходе совпадают.Нужно ли нам разделить этот переход? Это зависит от Δ = p-k + 1 - го символа на этом переходе (**). Чтобы проверить этот символ, нам нужно извлечь строку, лежащую в индексе l ' на хеш-таблица и получаем символ с индексом k' + Δ . Этот характер гарантированно существует, потому что состояние, которое мы рассматриваем, неявно и, таким образом, заканчивается в середине перехода TR (Δ ≤ p' - k').
Если равенство выполняется, нам нечего делать и возвращать true (конечная точка здесь) и ничего больше не делать. Если нет, то мы должны разделить переход и создать новое состояние r . TR теперь становится (l', (k', k' + Δ - 1)) → r. Другой переход создается для r: (l', (k' + Δ, p') → s ' . Теперь мы возвращаем false и r .(*): l не обязательно равно N+1. Аналогично, l и l' могут быть разными (или равными).
(**): обратите внимание, что число Δ = p - k + 1 совершенно не зависит от строки, выбранной в качестве ссылки для отображаемой подстроки. Это зависит только от неявного состояния кормили по заведенному порядку.
Канонизировать
Входные данные: узел _s_ и отображенная подстрока(l,(k,p)) , представляющая существующее состояниеe в дереве.
выходные данные: узел s ' и отображенная подстрока (l', (k', p')) , представляющая каноническую ссылку для состояния eИспользуя те же самые настройки выборки, мы просто должны идти вниз по дереву, пока мы не исчерпаем символ "бассейн". Здесь, так же, как и для
test_and_split, единство каждого перехода и тот факт, что у нас есть существующее состояние в качестве входных данных, дает нам действительную процедуру.Обновление
Входные данные: активная точка и индекс для текущей итерации.
вывод: активная точка для следующей итерации.
updateиспользует какcanonize, так иtest_and_split, которые являются GST-дружественными. Редактирование суффиксных ссылок точно такое же, как и для обычного дерева. Единственное, что стоит заметить заключается в том, что мы создадим открытые переходы (т. е. переходы, ведущие к узлам), используя S N+1 в качестве опорной строки. Таким образом, на итерации i переход всегда будет связан с отображенной подстрокой (N+1, (i,∞))
Заключительный шаг
Нам нужно "закрыть" открытые переходы. Для этого мы просто пересекаем их и редактируем∞, заменяя его L-1 , где L - длина S N+1. Нам также нужен символ sentinel, чтобы отметить конец строки. Символ, который мы точно никогда не встретим ни в одной строке. Таким образом, листья останутся листьями навсегда.
Заключение
Дополнительная выборка добавляет несколькоO(1) операций, немного увеличивая постоянный коэффициент сложности. Однако асимптотическая сложность остается явно линейной с длиной вставленных строк. Таким образом, построение GST (N ) из строк (S1,..., S N) с длиной n1,...,n N это:
C(GST (N)) = Σ i=1..N n i
Если в вашем обобщенном суффиксном дереве будет всего несколько строк, то вы можете объединить их в одну строку, используя уникальные терминальные символы (эти терминальные символы не должны использоваться во входных строках) между каждой строкой.
Например, предположим, что у вас есть 5 строк: str1, str2, str3, str4 и str5, затем вы можете объединить эти 5 строк как str1$str2#str3@str4%str5, а затем сделать суффиксное дерево этой объединенной строки.
Так как мы должны использовать уникальные терминальные символы, так что было бы ограничение на то, сколько максимальной строки, которая может быть добавлена в обобщенном дереве суффиксов. Любой символ, который никогда не будет использоваться во входных строках, может быть взят в качестве терминальных символов.
Таким образом, основываясь на предопределенном наборе терминальных символов, мы можем написать код.Следующая статья может оказаться полезной.
Comments