Блог пользователя adamant

Автор adamant, 10 лет назад, По-русски

Можно сказать, что предыдущая часть. Даже если вы считаете, что знакомы с суффиксным деревом, рекомендую просмотреть код внизу.

Всем привет! Наконец, я до него добрался :)

В данной статье я хотел бы избежать длинных и сложных теоретических выкладок, которые долго отпугивали меня от данного алгоритма. Так что сразу к делу. Доказательств приводить не буду, а больше внимания уделю особенностям реализации. Доказательства поищите где-нибудь на stackoverflow (лично я черпал знания об алгоритме именно оттуда) или на wiki-конспектах ИТМО или в книге Гасфилда или в конспекте Юрия Лифшица... Или ещё где-нибудь, где таким любят заниматься.

Суффиксным деревом будем называть суффиксный бор, сжатый таким образом, что вершина присутствует в дереве в том и только том случае, если в суффиксном боре у неё было больше одного сына или если она отвечает одному из суффиксов строки.

Теперь о самом алгоритме. На каждой итерации он поддерживает так называемое неявное суффиксное дерево. В нём критерием существования вершины является только то, что у неё больше одного ребёнка, то есть "терминальные" вершины могут быть опущены. Обычно рёбра при построении суфф дерева хранят в виде пары [Li, Ri]. Лично мне не очень удобно работать с ними в таком виде, поэтому предлагаю вместо этого в каждой вершине дерева хранить такие данные о ребре, которое ведёт в неё — fposi — первая позиция вхождения ребра в строку и leni — соответсвенно, длина ребра. При этом длину рёбер, ведущих в листья будем по-умолчанию считать равной inf. Таким образом мы можем быть уверены, что в любой момент времени рёбра в листья идут до самого конца строки. Корнем дерева будет вершина под номером 0.

На каждом шаге алгоритма будем хранить самый длинный неуникальный суффикс строки. Для этого будем хранить пару чисел (node, pos) — вершину в суфф дереве, которая ему соответствует и число символов, которое необходимо пройти от неё вниз для получения этого суффикса. Изначально считаем node = pos = 0. При дописывании нового символа мы увеличим pos на 1 и добавим все уникальные суффиксы строки, которые возникли после добавления нового символа.

Также нам понадобится понятие суффиксной ссылки. Она определена для внутренних вершин дерева. Переход по суффиксной ссылке будет вести в вершину, соответствующую той же строке, но без первого символа. Для корня суффиксная ссылка не определена.

Добавление будет состоять из следующих этапов:

  1. Если pos = 0, то все суффиксы обработаны. Завершаемся. Иначе найдём вершину, после которой находится новый суффикс. То есть, пока pos больше длины ребра, исходящего из данной вершины, будем идти по ребру и вычитать его длину из pos.
  2. Пытаемся добавить очередной суффикс. Здесь нас ожидает три варианта.
    1. Если у нас нет исходящего ребра по интересующему нас символу, то мы просто создаём новую вершину и подвешиваем её к текущей.
    2. Если ребро есть и суффикс, который мы хотим добавить целиком лежит на нём, завершаем свою работу — этот и дальнейшие суффиксы не являются уникальными.
    3. Если ребро есть и суффикс не лежит на нём целиком, это значит, что нам нужно создать новую вершину посередине этого ребра, к которой подвесить старую вершину с конца ребра и новую вершину, соответствующую суффиксу. Стоит заметить, что ребро к новому листу в данный момент будет иметь длину, равную единице.
  3. Если мы не завершили работу на прошлом шаге, переходим к следующему суффиксу. Если мы находимся в корне, то мы уменьшаем pos на единицу, иначе же мы просто переходим по суффиксной ссылке node = link(node). После этого мы переходим к пункту 1.

Отдельно о пересчёте суффиксной ссылки. На i-ом этапе мы будем устанавливать суффиксную ссылку для внутренней вершины, созданной i - 1-ом. Если мы создаём новую внутреннюю вершину, то суффиксная ссылка будет вести в неё. В двух остальных случаях суффиксная ссылка ведёт в node (Мне лень расписывать поистине чудесное доказательство, поэтому оно оставляется любопытному читателю в качестве домашнего упражнения).

На этом описание алгоритма заканчивается и мы переходим к его реализации.

Код: #sT8Vd1

Я постарался сделать код настолько простым и понятным, насколько это вообще возможно :) Не знаю, насколько у меня получилось, надеюсь на ваши отзывы и вопросы.

  • Проголосовать: нравится
  • +104
  • Проголосовать: не нравится

»
10 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Спасибо большое. Все предельно просто и понятно написано/объяснено.

»
10 лет назад, # |
  Проголосовать: нравится +11 Проголосовать: не нравится

Wow :) That's very short code. Thanks.

»
10 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

If you use unordered_map instead of map the complexity will be O(N), which is one of the crucial parts of the algorithm :)

  • »
    »
    10 лет назад, # ^ |
      Проголосовать: нравится +5 Проголосовать: не нравится

    From my experience if you need huge amount of associative containers, usual map is preferred. Also usual map is more useful when you need to traverse tree in lexicographical order.

    • »
      »
      »
      10 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      Yes, you are right. And because the alphabet size is constant, using map is also constant. But you could use only one unordered_map<pair<int,int>,int> like this and have a guaranteed constant time data access. As you said though traversing the tree wouldn't be as good as it is with map.

»
10 лет назад, # |
Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

.

»
9 лет назад, # |
  Проголосовать: нравится -16 Проголосовать: не нравится

Could you please also write a small function to search if a string exists as a substring in the original string ?

  • »
    »
    9 лет назад, # ^ |
      Проголосовать: нравится -17 Проголосовать: не нравится

    This is related to an on-going contest :|

    • »
      »
      »
      9 лет назад, # ^ |
        Проголосовать: нравится +47 Проголосовать: не нравится

      I'm so tired of things like that. Someone asks for some really well-known stuff and then someone else tells us that OMG NOOO THIS IS RELATED TO ON-GOING CONTEST.

      No, really. It is the problem of contest-maker to make some non-trivial problems so such well-known tricks won't be helpful.

»
8 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Ребята, тут такое дело, я этот код затестить хотел тут, добавил незначительные изменения, а оно стабильно получает 94%. Либо я криво dfs вставил, либо (О Боже!) код adamant'a с багой. (Хотя есть ненулевая вероятность баги на самом e-olymp).

  • »
    »
    8 лет назад, # ^ |
    Rev. 3   Проголосовать: нравится +11 Проголосовать: не нравится

    Там тесты бажные, инфа 100% я сдавал свой код в другом месте, и он прошел.

    На e-olymp 10% задач таких.

  • »
    »
    8 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    если хочешь рекурсивно обойти суффиксное дерево или любой другой большой граф, убедись что не получишь stack overflow на макстесте. Какой на e-olymp размер стека?

    • »
      »
      »
      8 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      Добавил к прошлому коду #pragma comment(linker,"/STACK:64000000"), получил все те же 94%.

      • »
        »
        »
        »
        8 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

        вроде эта строка работает только на MSVS

»
8 лет назад, # |
  Проголосовать: нравится -10 Проголосовать: не нравится

Can you suggest some problems on suffix trees ?

»
8 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Can you suggest some problems on suffix trees. ?

»
7 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Hey can you please explain why you are updating link[last] to node in line number 49 of your code.

»
7 лет назад, # |
  Проголосовать: нравится -15 Проголосовать: не нравится

в коде в строчке len [v] -= pos — 1; можно сделать if (len[v] != inf) len[v] -= pos — 1; что повысит читаемость при дебаге и понимании в первый раз.

»
7 лет назад, # |
  Проголосовать: нравится -9 Проголосовать: не нравится

Is palindromic tree a special type of suffix tree?