Алгоритм укконена для обобщенных суффиксных деревьев
в настоящее время я работаю над собственной реализацией дерева суффиксов (используя C++, но вопрос остается языковым агностиком). Я учился оригинальная бумага от Укконена. Статья очень ясна, поэтому я приступил к работе над своей реализацией и попытался решить проблему для обобщенных суффиксных деревьев.
в дереве каждая подстрока, ведущая от узла к другому, представлена с помощью пары целых чисел. Хотя это просто для обычного дерева суффиксов, a проблема возникает, когда несколько строк сосуществуют в одном дереве (которое становится Обобщенное Суффиксное Дерево). Действительно, теперь такой пары недостаточно, нам нужна другая переменная, чтобы указать, какую ссылочную строку мы используем.
небольшой пример. Рассмотрим строку 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;
проблема, с которой я в настоящее время сталкиваюсь, заключается в следующем:
Я получаю запрос на добавление строки в дерево. Во время алгоритма мне может потребоваться проверить существующие переходы, связанные с другими зарегистрированными строками, представленными в виде триплета (идентификатор строки ссылки, k, p). Некоторые операции обновления основаны на индексах подстрок,как я могу выполнять их в таких условиях?
Примечание: вопрос является языковым агностиком, поэтому я не включил c++ - tag, хотя показан небольшой фрагмент.
2 ответов
TL; DR
исходный алгоритм на самом деле не нужно изменять, чтобы построить Обобщенное Суффиксное Дерево.
здесь github для моей текущей реализации (в C++). Он все еще нуждается в некотором обзоре и рефакторинге (и некотором обширном тестировании...) но это только начало!
Примечание: В настоящее время я работаю над этой реализацией, я обновлю этот ответ с материалом, когда он будет доступен! (Колиру живет и все такое...)
подробный анализ
горб у меня был правильный путь. Чтобы не отставать от трюков, используемых в исходном алгоритме, нам действительно нужно добавить ссылку на исходную строку. Кроме того, алгоритм онлайн, что означает, что вы можете добавлять строки на лету в дерево.
Предположим, у нас есть Обобщенное Суффиксное Дерево GST (N) для строк (S1, ..., SN). Проблема здесь заключается в том, как обрабатывать процесс построения 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.
вставка новой строки
мы хотим вставить SN+1 в GST (N).
GST (N) может иметь уже много узлов и переходов. В простом дереве у нас был бы только корень и специальный узел приемника. Здесь у нас могут быть переходы для SN+1, которые уже были добавлены путем добавления некоторых предыдущих строк. Первое, что нужно сделать, это спуститься по дереву через переходы, пока оно соответствует SN+1.
By поступая так, мы заканчиваем в состоянии r. Это состояние может быть явным (т. е. мы закончили прямо на вершине) или неявным (несоответствие произошло в середине перехода). Мы используем ту же концепцию, что и в исходном алгоритме для моделирования такого состояния:ссылка паре. Быстрый пример:
- мы хотим вставить SN+1 =
banana
- узел s представляют
ba
явно существует в GST (N) -
s переход t подстроки
nal
когда мы спускаемся по дереву, мы оказываемся в переходе t на символ l
что является несоответствием. Таким образом, неявное состояние r мы получаем в лице ссылка паре (s, m), где m - это сопоставленной подстроки (N+1, (1,3)).
здесь r является активной точкой для 5-й итерации алгоритма в построении banana
суффикс дерева. Тот факт, что мы попали в это состояние, означает именно то, что дерево для bana
уже построен в GST (N).
в этом примере мы возобновляем алгоритм на 5-й итерации, чтобы построить дерево суффиксов для banan
использование дерева для bana
. Чтобы не утратить общности, скажем, что r = (s, (N+1, (k, i-1)), Я быть индексом первого несоответствия. Мы действительно k ≤ i (эгалитарность является синонимом r будучи явным состоянием).
свойство: мы можем возобновить алгоритм Укконена для построения GST (N) на повторе Я (вставка символа по индексу Я на SN+1). Активная точка для эта итерация является состоянием r мы спустились по дереву. необходима только настройка некоторых операций выборки для разрешения подстрок.
доказательство для свойства
во-первых, наличие такого государства r подразумевает, что все состояния для промежуточного дерева T (N+1)i-1 там же. Итак, все готово, и мы возобновляем алгоритм.
нам нужно доказать, что каждая процедура в алгоритме остается действительной. Есть 3 такие подпрограммы:
-
test_and_split
: учитывая символ для вставки на текущей итерации, тест wether нам нужно разделить переход на два отдельных перехода, и если текущая точка является конечной точкой. -
canonize
: дано ссылка паре (n, m) здесь n является вершиной и m a сопоставленная подстрока, возвращает пару (n', m') представление того же состояния, например m' - это минимально возможные подстроки. -
update
: обновление GST (N) так что он имеет все состояния для промежуточного дерева T (N+1)я в конце прогона.
test_and_split
вход: вершинный s, сопоставленной подстроки m = (l, (k, p)) и символ t.
выход: логическое значение, которое сообщает, если состояние (s, m) является конечной точкой для текущей итерации и узла r представляющих явно (s, m) если это не конечная точка.
самый простой случай идет первым. Если подстрока пуста (k > p), состояние уже представлено явно. Мы просто должны проверить, достигли ли мы конечной точки или не. в GST, как и в общем дереве суффиксов, есть всегда не более одного перехода на узел, начиная с заданного символа. таким образом, если есть переход, начиная с t, мы возвращаем true (мы достигли конечной точки), в противном случае false.
теперь трудная часть, когда k ≤ p. Сначала нам нужно принести строку Sl лежа на индекс l(*) в оригинале таблица строк.
Пусть (l', (k', p')) (респ. s') подстрока (респ. узел), связанный с переходом TR of s начиная с символов Sl(k) (*). Такой переход существует, потому что (s, (l, (k,p)) представляет (существующее) неявное состояние на границы путь промежуточного дерева T (N+1)i-1. Кроме того, мы обязательно что p-k первые символы на этом переходе совпадают.
нам нужно разделить этот переход? Это зависит от Δ = p-k + 1-й символ на этом переходе (**). Чтобы проверить этот символ, нам нужно получить строку, лежащую в index l' на хэш-таблице и получить символ в index 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_and сопоставленная подстрока (l, (k,p)) представление существующего состояния e в дереве.
выход: узел s' и сопоставленной подстроки (l', (k',p')) представление канонической ссылки для состояния e
используя то же самое fetching tweaks, мы просто должны идти вниз по дереву, пока мы не исчерпан символ "бассейн". Здесь, как и для test_and_split
единство каждого перехода и тот факт, что у нас есть существующее состояние в качестве входных данных, предоставляет нам действительную процедуру.
обновление
вход: активная точка и индекс для текущей итерации.
выход: активная точка для следующей итерации.
update
использует оба canonize
и test_and_split
, которые GST-friendly. Редактирование суффиксной ссылки точно такое же, как и для общего дерева. Единственное, что нужно заметить, это то, что мы создадим открыть переходы (т. е. переходы, ведущие к узлам) с помощью SN+1 в строке ссылки. Таким образом, на итерации Я, переход всегда будет связан с отображенной подстрокой (N+1, (i,∞))
заключительный шаг
нам нужно "закрыть"открыть переходы. Для этого мы просто пересекаем их и редактируем∞, заменяя его на L-1, где L - длина SN+1. Нам также нужен символ sentinel, чтобы отметить конец строки. Символ, который мы никогда не встретим ни в одной строке. Таким образом, листья останутся листья навсегда.
вывод
дополнительная выборка работы добавляет несколько O (1) операции, немного увеличивая постоянный коэффициент сложности. Хотя асимптотическая сложность остается, очевидно, линейной с длиной вставленных строк. Таким образом, построение GST (N) из строки (S1,..., SN) длиной n1,..., nN есть:
c (GST (N))= Σi=1..N nя
Если в вашем обобщенном дереве суффиксов будет всего несколько строк, вы можете объединить их в одну строку, используя уникальные символы терминала (эти символы терминала не должны использоваться во входных строках) между каждой строкой.
например, предположим, что у вас есть 5 строк: str1, str2, str3, str4 и str5, затем вы можете объединить эти 5 строк как str1$str2#str3@str4%str5, а затем сделать суффикс дерева этой объединенной строки.
Так как мы Необходимо использовать уникальные терминальные символы, чтобы было ограничение на максимальное количество строк, которые могут быть добавлены в обобщенное дерево суффиксов. Любой символ, который никогда не будет использоваться во входных строках, может быть принят в качестве терминальных символов.
таким образом, на основе предопределенного набора терминальных символов мы можем написать код.
Следующая статья может быть полезна.