Алгоритм укконена для обобщенных суффиксных деревьев

в настоящее время я работаю над собственной реализацией дерева суффиксов (используя 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, а затем сделать суффикс дерева этой объединенной строки.

Так как мы Необходимо использовать уникальные терминальные символы, чтобы было ограничение на максимальное количество строк, которые могут быть добавлены в обобщенное дерево суффиксов. Любой символ, который никогда не будет использоваться во входных строках, может быть принят в качестве терминальных символов.

таким образом, на основе предопределенного набора терминальных символов мы можем написать код.

Следующая статья может быть полезна.

Обобщенное Суффиксное Дерево