МИХАИЛ ДУБОВ ОТДЕЛЕНИЕ ПРОГРАММНОЙ ИНЖЕНЕРИИ НИУ ВШЭ, РОССИЯ, МОСКВА ОБОБЩЕННЫЕ АННОТИРОВАННЫЕ СУФФИКСНЫЕ ДЕРЕВЬЯ ОСОБЕННОСТИ РЕАЛИЗАЦИИ. АЛГОРИТМЫ И СТРУКТУРЫ ДАННЫХ АЛГОРИТМЫ ПОСТРОЕНИЯ НАИВНОЕ ПОСТРОЕНИЕ ОБОБЩЕННЫХ АСД. ЛИНЕЙНЫЙ АЛГОРИТМ НАИВНАЯ РЕАЛИЗАЦИЯ Алгоритм NaiveConstruction(𝐶) Вход. Коллекция строк 𝐶 = {𝑆1 … 𝑆𝑚 }. Выход. Обобщенное АСД для 𝐶. 1. for 𝑖 ← 1 to 𝑚 2. for 𝑗 ← 1 to 𝑛𝑖 = 𝑆𝑖 3. do 𝑘 ← совпадение 𝑆𝑖 [𝑗: ] с АСД 4. for узел 𝑢 из 𝑆𝑖 𝑗: 𝑘 5. do 𝑓 𝑢 ← 𝑓 𝑢 + 1 6. for 𝑙 ← 𝑘 + 1 to 𝑛𝑖 7. do вставить узел 𝑣 8. 𝑓(𝑣) ← 1 Время работы: 𝑶(𝒏𝟏 𝟐 + ⋯ + 𝒏𝒎 𝟐 )~𝑶(𝒎𝒏𝒎𝒂𝒙 𝟐 ) SCORE(𝑆, 𝑔𝑎𝑠𝑡) = • score(𝑠) = • 𝒑 𝑢 = 𝑆 𝑖=1 𝑠𝑐𝑜𝑟𝑒(𝑠 𝑆 𝑘 𝑖=1 𝑝(𝑛𝑜𝑑𝑒𝑖 ) 𝑘 𝑓(𝑢) 𝑓(𝑝𝑎𝑟𝑒𝑛𝑡 𝑢 ) 𝑖: ) (k – число совпавших узлов) ДЕРЕВО КЛЮЧЕЙ ≠ СУФФИКСНОЕ ДЕРЕВО В суффиксном дереве: • Путь “корень → … → лист” = суффикс • На ребрах – непустые подстроки; • Каждый внутренний узел (кроме, может быть, корня) имеет не менее двух дочерних. • 𝑶 𝒏𝟐 узлов и ребер • Не менее 𝟐𝒏 узлов и 𝟐𝒏 − 𝟏 ребер • Но: каждое ребро содержит подстроку ⇒ все еще 𝑶 𝒏𝟐 памяти СУФФИКСНОЕ ДЕРЕВО Алгоритм LinearConstruction(𝐶) Вход. Коллекция строк 𝐶 = {𝑆1 … 𝑆𝑚 }. Выход. Обобщенное АСД для 𝐶. ? SCORE(𝑆, 𝑔𝑎𝑠𝑡) = • Оптимизация ⇒ 𝑶 𝒏 памяти • Вычисление score – возможно! • Возможно ли построение за 𝑶 𝒏 ? • score(𝑠) = • 𝒑 𝑢 = 𝑆 𝑖=1 𝑠𝑐𝑜𝑟𝑒(𝑠 𝑖: ) 𝑆 𝒌 𝒊=𝟏 𝒑(𝒏𝒐𝒅𝒆𝒊 ) 𝑓(𝑢) 𝑓(𝑝𝑎𝑟𝑒𝑛𝑡 𝑢 ) +𝒍−𝒌 𝒍 (l – число совпавших символов, k – число узлов в совпадении) ПОСТРОЕНИЕ СУФФИКСНЫХ ДЕРЕВЬЕВ ЗА ЛИНЕЙНОЕ ВРЕМЯ Петер Вайнер, 1973 “…«Алгоритм года 1973»!” Эдвард МакКрейг, 1976 Эско Укконен, 1995 Алгоритм Укконена: 1. Проще для восприятия; 2. Эффективнее; 3. Первый «он-лайн»алгоритм. АЛГОРИТМ УККОНЕНА Алгоритм Ukkonen(𝑆) Вход. Строка 𝑆; 𝑆 = 𝑛 Выход. Суффиксное дерево для 𝑆. 1. Построить суффиксное дерево 𝑇1 – состоящее из одной дуги, помеченной символом 𝑆[1]. 2. for 𝑖 ← 1 to 𝑛 − 1 {Фаза i+1} 3. for 𝑗 ← 1 to 𝑖 + 1 {Продолжение j} 4. do найти конец пути из корня с меткой 𝑆[𝑗: 𝑖]. 5. Если нужно, добавить символ 𝑆[𝑖 + 1], обеспечив присутствие в дереве строки 𝑆[𝑗: 𝑖 + 1]. «Наивная» реализация: время работы - 𝑶(𝒏𝟑 ) • В фазе 𝑖 + 1 дерево 𝑇𝑖+1 для 𝑆[: 𝑖 + 1] строится из дерева 𝑇𝑖 для 𝑆 : 𝑖 ; • Каждая фаза 𝑖 + 1 делится на 𝑖 + 1 продолжений; в продолжении 𝑗 фазы 𝑖 + 1 суффикс 𝑆[𝑗: 𝑖] дополняется символом 𝑆[𝑖 + 1]. АЛГОРИТМ УККОНЕНА • Как понизить вычислительную сложность? «Разделяй и властвуй» 𝑂(𝑛3 ) → 𝑂(𝑛2.81 ) Динамическое программирование 𝑂(𝑛!) → 𝑂(𝑛2 2𝑛 ) Жадные алгоритмы 𝑂( 𝑉 𝐸 ) → 𝑂( 𝑉 log 𝑉 + 𝐸 ) АЛГОРИТМ УККОНЕНА • Как понизить вычислительную сложность? «Программистские» приемы реализации! По отдельности выглядят как полезная эвристика для ускорения; Вместе дают «прорыв» во времени выполнения алгоритма. 𝑂(𝑛3 ) → 𝑂(𝑛) ПРИЕМЫ РЕЛИЗАЦИИ АЛГОРИТМА УККОНЕНА ЗА ЛИНЕЙНОЕ ВРЕМЯ 1. Суффиксные связи 2. Ускоренное прохождение ребер 𝑂(𝑛2 ) 𝑂(𝑛) 3. Пропуск части продолжений, где не нужно ничего вычислять КАК НАСЧЕТ ОБОБЩЕННЫХ ДЕРЕВЬЕВ? • Сведение к алгоритму Укконена для одной строки: Алгоритм GeneralizedSuffixTree_Ukkonen(𝐶) Вход. Коллекция строк 𝐶 = {𝑆1 … 𝑆𝑚 }. Выход. Обобщенное суффиксное дерево для 𝐶. 1. Построить суффиксное дерево 𝑇 для 𝑆1 алгоритмом Укконена. 2. for 𝑖 ← 2 to 𝑚 3. do найти совпадение 𝑆𝑖 с T; пусть 𝑘 ← число совпавших символов. 4. (в этот момент все суффиксы 𝑆𝑖 [1: 𝑘] уже закодированы в 𝑇; 5. 𝑘 фаз алгоритма Укконена, таким образом, выполнены неявно) 6. выполнить алгоритм Укконена для 𝑆𝑖 , начиная в фазе 𝑘 + 1. КАК НАСЧЕТ АННОТИРОВАННЫХ ДЕРЕВЬЕВ? • Сведение к алгоритму Укконена для обобщенных суффиксных деревьев: Алгоритм LinearConstruction(𝐶) Вход. Коллекция строк 𝐶 = {𝑆1 … 𝑆𝑚 }. Выход. Обобщенное АСД для 𝐶. 1. Сформировать 𝐶 ′ = {𝑆1 $1 … 𝑆𝑚 $𝑚 }, где $𝑖 - уникальные символы. 2. Построить обобщенное суффиксное дерево 𝑇 для коллекции 𝐶 ′ используя алгоритм с линейной сложностью, например, алгоритм Укконена. 3. for 𝑙 in 𝑙𝑒𝑎𝑣𝑒𝑠(𝑇) 4. do 𝑓 𝑙 ← 1 5. Выполнить обход дерева T сверху вниз; в каждом внутреннем узле 𝑣 присвоить 𝑓 𝑣 ← 𝑢∈𝑇: 𝑝𝑎𝑟𝑒𝑛𝑡 𝑢 =𝑣 𝑓(𝑢). Алгоритм основан на важном свойстве АСД: 𝑓 𝑣 = 𝑢∈𝑇: 𝑝𝑎𝑟𝑒𝑛𝑡 𝑢 =𝑣 𝑓(𝑢) Время работы: 𝑶(𝒏𝟏 + ⋯ + 𝒏𝒎 )~𝑶(𝒎𝒏𝒎𝒂𝒙 ) НАИВНЫЙ VS. ЛИНЕЙНЫЙ АЛГОРИТМ ЭКСПЕРИМЕНТ: СЛУЧАЙНЫЕ СТРОКИ НАИВНЫЙ VS. ЛИНЕЙНЫЙ АЛГОРИТМ ЭКСПЕРИМЕНТ: «НАИХУДШИЕ» СТРОКИ ВОПРОС ПАМЯТИ ДАЛЬНЕЙШИЕ ПУТИ ОПТИМИЗАЦИИ ИСПОЛЬЗОВАНИЯ ПАМЯТИ ХРАНЕНИЕ ДОЧЕРНИХ УЗЛОВ ВЕРШИНЫ • Хранение указателей на дочерние узлы: 1. Хэш-таблица + Операции вставки и поиска узла – за 𝑶 𝟏 ; - Требуется дополнительная память на организацию структуры данных («идеальное хэширование» ⇒ минимум log 2 𝑒 ∙ 𝑛 ≈ 1.44𝑛 бит памяти на 𝑛 элементов). 2. Отсортированный (по символам) массив + - 3. 4. Чрезвычайно эффективно по памяти; Но: мало «детей» ⇒ много пустых ячеек; Поиск узла деградирует до 𝑶(𝐥𝐨𝐠 𝜮 ); Построение деградирует с 𝑂(𝑛) до 𝑶 𝒏 𝒍𝒐𝒈 𝜮 . Связный список Сбалансированные бинарные деревья ch = {‘A’: <p1>, ‘B’: <p2>, ‘C’: <p3>, ‘X’: <p4>} ch[0] = <p1> ch[1] = <p2> ch[2] = <p3> ch[3] = <p4> //‘A’ //‘B’ //‘C’ //‘X’ ХРАНЕНИЕ ДОЧЕРНИХ УЗЛОВ ВЕРШИНЫ • Возможны комбинированные подходы: На верхних уровнях дерева плотность детей больше; имеет смысл использовать массивы. Ближе к листьям детей у вершин становится меньше ⇒ используем хэш-таблицы или сбалансированные бинарные деревья. СУФФИКСНЫЕ МАССИВЫ • Для строки длины 𝑛: Суффиксное дерево занимает ~ 𝟐𝟎 … 𝟒𝟎 ∙ 𝒏 байт памяти; Суффиксный массив занимает ~ 𝟒 … 𝟖 ∙ 𝒏 байт памяти. СУФФИКСНЫЕ МАССИВЫ • Откуда берутся преимущества, присущие суффиксным массивам? Мы жертвуем: концептуальной простотой структуры данных; простыми алгоритмами построения и поиска подстроки (обе операции все еще осуществимы за линейное время, но соответствующие алгоритмы – сложнее); хранением связей между родительскими и дочерними узлами. Получаем: сильную экономию по памяти; компактно хранящуюся в памяти структуру данных ⇒ становится возможным пейджинг. Но можно ли в суффиксном массиве вычислять SCORE без наличия родительско-дочерних связей?.. ДАЛЬНЕЙШАЯ РАБОТА • Экспериментирование с организацией суффиксных деревьев в памяти; возможно, с привлечением библиотеки NumPy. • Разработка комбинированного алгоритма построения АСД, который: анализирует входные тексты; определяет, какой из алгоритмов (Наивный/Укконен) лучше подходит для данного случая. • Тестирование нашей реализации на реальных данных; • Реализация ПС-таблицы. ЛИТЕРАТУРА • Gusfield, Dan, Algorithms on Strings, Trees and Sequences: Computer Science and Computational Biology, Cambridge University Press Q&A