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

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

Всем привет! Не буду долгих предисловий тут писать, поэтому сразу к делу. Читая книгу Мейерса "Эффективное использование STL", я вдруг наткнулся на упоминание о наличии в некоторых версиях STL структуры данных rope. Если кратко, то эта структура данных позволяет быстро вырезать/вставлять куски массива в произвольные позиции, аналогично декартовому дереву по неявному ключу (с аналогичной сложностью — подробности смотрите в статье на вики). Она иногда используется для обработки сверхдлинных строк.

Как выяснилось, rope действительно реализована в некоторых версиях STL, например в SGI STL. Сразу замечу, что это наиболее полная документация по классу rope, которую мне удалось найти в сети. А теперь давайте разыщем rope в GNU C++. Поскольку кто-то ранее находил расширенную версию красно-черных деревьев в GNU, я подумал, почему бы и rope где-нибудь не заваляться. Для тестинга я взял вот эту задачу, в которой у меня уже была сдана дерамида по неявному ключу. После недолгого гугления получилось следующее решение:

#include <iostream>
#include <cstdio>
#include <ext/rope> //заголовочный файл с rope
using namespace std;
using namespace __gnu_cxx; //пространство имен, в котором находится класс rope и все, что с ним связано
int main()
{
    ios_base::sync_with_stdio(false);
    rope <int> v; //используем как самый обычный stl контейнер
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; ++i)
        v.push_back(i);
    int l, r;
    for(int i = 0; i < m; ++i)
    {
        cin >> l >> r;
        --l, --r;
        rope <int> cur = v.substr(l, r - l + 1);
        v.erase(l, r - l + 1);
        v.insert(v.mutable_begin(), cur);
    }
    for(rope <int>::iterator it = v.mutable_begin(); it != v.mutable_end(); ++it)
        cout << *it << " ";
    return 0;
}

Оно без всяких проблем зашло и уложилось в секунду, что в 2 раза медленнее варианта с рукописным декартовым деревом, но при этом с чуть более оптимальным расходом памяти. Полную документацию по всем методам можно найти по вышеприведенной ссылке на SGI STL. Судя по всему, в GNU полностью аналогичная реализация. Visual C++ как унылое го... не поддерживает rope.

Что касается применения, то есть несколько особенностей. Из документации SGI STL следует, что класс rope плохо дружит с изменениями отдельных элементов последовательности (поэтому методы begin() и end() возвращают const_iterator. Чтобы получить обычный итератор, необходимо вызывать mutable_begin() и mutable_end() соответственно.). Также нельзя просто взять и присвоить значение i-ому элементу последовательности (см. код ниже для подробностей). Но насколько это "плохо", я не знаю. По идее за классический логарифм от числа элементов. В то же время конкатенация (операция +=) вообще работает за O(1) (не считая затрат на создание объекта справа, конечно).

Особенности индексирования можно увидеть в этом коде: http://pastebin.com/U8rG1tfu. Поскольку разработчики очень не хотят, чтобы мы меняли отдельные элементы контейнера, оператор [ ] возвращает ссылку на константу, но специальный метод все же есть. Данный код работает столько же, сколько и предыдущий. И да, забыл упомянуть, что все итераторы RandomAccess, что не может не радовать.

Если кто захочет потестировать контейнер на более серьезных задачах, расскажите в комментариях. По моему ощущению, у нас теперь есть быстрый массив с логарифмическим временем выполнения всех операций и как обычно большой константой :)

UPD: сдал еще задачу. Здесь мало запросов, но последовательность подлиннее. Зашло тоже без проблем. Еще наткнулся на такую особенность, что erase может принимать только 1 индекс типа size_t (хотя метода с такой сигнатурой нет). И я не очень понимаю, что он делает в этом случае. Из-за этого можно случайно набажить, когда нужно удалять 1 элемент.

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

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

действительно сверхдлинные строки можно хранить. этот код в запуске отрабатывает за 300 мс

upd. этот за 140мс

upd2. 292E - Копирование данных сдать не удалось

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

    Попробовал вариант с replace, тоже не прокатило. Так что не все так хорошо) А там обычный treap заходит?

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

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

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

        Интересно, как оно ведет себя в тех задачах, где проходит treap. Я просто давно уже не участвую и кроме приведенной в посте задачи не помню задачи на неявный treap.

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

        Будет. Вот здесь все очень подробно объяснили, от себя мне добавить нечего.

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

          мм, ну ок. просто для меня персистентное декартово дерево — уже не декартово дерево.

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

.

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

    А вот здесь декартово у меня залетело. По-видимому при 105 запросах и ограничении в секунду rope лучше не использовать.

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

.

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

    Вот здесь наиболее полное описание реализации, что мне удалось найти. Вот какое именно бинарное дерево используется, тут не сказано. Однако, в коде (в ropeimpl.h) я нашел список чисел Фибоначчи рядом с _S_max_rope_depth и _S_min_len функцией _S_balance. Я не в курсе, как реализуется rope, но может это АВЛ?

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

      АВЛ как-то жестко по неявному ключу. Вот сплей дерево умеет и за О(1) конкатенироваться и все остальное перечисленное.

      P.S. не знаю на чем rope, просто флужу

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

        Никто не будет писать splay, потому что у него хорошая только амортизированная оценка. Да и возможно есть некоторые нюансы по сравнению с обычными деревьями поиска, препятствующие использованию splay.

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

          Хорошая амортизированная оценка — это, вообще-то, хорошо. У добавления элемента в вектор тоже только амортизированная оценка константная.

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

            Да, это я ляпнул, не подумав. Но все равно никогда не видел, чтобы Splay-дерево использовалось в какой-либо стандартной библиотеке. Оно хоть и изящней всех остальных, но проигрывает тому же красно-черному, например.

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

        Вот сплей дерево умеет и за О(1) конкатенироваться и все остальное перечисленное.

        ведь? То есть я однажды слышал, что якобы merge-и делаются за амортизированные O(1), но не очень понимаю, с чего бы быть такому. Можно поподробнее?

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

          На самом деле я не знаю такую амортизированную оценку. Я просто подумал, что нам ничего не мешает создать вершину-корень, в которой будет пустая строка и прицепить слева и справа то, что конкатенируем. Свойства вроде как не нарушаются.

          P.S. Нашел такую ссылку: http://habrahabr.ru/post/144736/

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

            Да. Но во первых плодятся лишние вершины на пустые строки, во вторых растет потенциал, который есть в оценке. Поэтому все-таки лог.

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

              Это еще почему? Оценка конкатенации — явно константа.

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

                То, что ты предложил, просто ломает все остальные асимптотики. Ты же не мёрджишь так декартовы деревья? Попробуй приконкатенировать 105 одноэлементных кусочков по очереди следующий к предыдущему. Если следовать твоему алгоритму, получится бамбук глубины 105.

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

                Амортизированное время = реальное + изменение потенциала в правильную сторону. На этом строится оценка Splay в общем-то. Потенциал, который есть в доказательстве, которое я видел увеличивается на O(logn) при merge. Это дает логорифмическое время в этом смысле.

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

There is also an order-statistics tree in the SGI library included in STL.

Sample code: http://ideone.com/QuiYER

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

    .

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

    Cool! I have also found the patricia trie and splay tree there as well as the binomial heap and some other data structures, but it seems to me that they are incomplete.

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

      Well, Last night I was investigating contents of pb_ds (Politics based data strucutres) library, included by default in g++. I've found that implementations of binary search trees there are almost as we need: they support traveling with node iterators (such as moving to the left/right child, dereferencing and everything we need), splitting, mergeing and even contatining additional information in each node with its recalculation after every change of tree structure (such as size of subtree, minimum or maximum)!

      Here are two examples of what we can do with this library.

      First one. Basic examples of usage. Look over it, it's very interesting! It also compiles here on Codeforces.

      Second one with solution of RMQ problem using this tree. When I was writing it I was thinking like "That's really great! That can replace treap as default binary search tree that people often write on contests! That's very cool!". But...

      The main problem is that splitting is implemented in linear time because of very stupid bottleneck: after split they set the size of second subtree as std::distance(other.begin(), other.end(), that works in linear time. =(

      There is also implementation of 4 types of heap. They support iterators to fixed elements of the heap, that makes us possible to write, for example, Dijkstra with heap algorithm. But here is a benchmark, that shows that it is still 20-30% slower then usual Dijkstra+set implementation.

      Patricia tree was a surprise for me, essentially after I looked in wikipedia and found a compressed trie (!) picture. But in fact it is another associative container for storing keys in increasing order:

      Patricia trees have excellent lookup performance, but they do so through maintaining, for each node, a miniature "hash-table". Their space efficiency is low, and their modification performance is bad.

      (from official documentation of that library)

      There is also forward-linked list and bunch of hashmaps (I didn't tested yet).

      That trees are really what we could use in our contests, If it hadn't linear-time split. I've written an e-mail to the developers of that library with some questions about why they didn't implement it more efficient. Waiting for the answer from them.

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

        What's their point of doing this in Linear time when it can be done in constant time? This is really weird! Nice effort by the way I really appreciate it :)

        I also found a skip-list implementation, it allows finding, searching, deleting all in logarithmic time.

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

        Excuse me, but in which file did you find the linear split method ? As from what I can see in this page

        http://gcc.gnu.org/onlinedocs/libstdc++/ext/pb_ds/tree_based_containers.html

        Scroll to the end of the page right to (Additional Methods)

        You will find this: "These methods are efficient — red-black trees are split and joined in poly-logarithmic complexity; ordered-vector trees are split and joined at linear complexity. The alternatives have super-linear complexity."

        So only ordered-vector trees are splitted in linear time, you should be alright if you use red-black tree as the underlying data structure by specifying the rb_tree_tag when creating the object?

        I'll look into the implementation of the split method inside..

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

          No, split will be anyway linear-time. After tree-depend implementation of split function there is a call of general function for all binary trees split_finish(other), where follows next line:

          ...
          other.m_size = std::distance(other.begin(), other.end());
          ...
          

          (/ext/pb_ds/detail/bin_search_tree_/split_join_fn_imps.hpp, #134).

          Of course std::distance for iterators of this tree works in linear time (it shifts first iterator until it is equal to the second iterator). You can run my solution for RMQ on big test to ensure that I'm right. It's surprising, but in this place official documentation is wrong.

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

        Did the authors reply by now? I think it was a design issue because to update the tree size quickly one needs the subtree size augmentation that is only provided by the tree_order_statistics_node_update mixin.

        We can work around the problem by overloading std::distance, but it's not pretty: https://github.com/niklasb/contest-algos/blob/master/stl_splay_tree.cpp Overall it takes a lot of code to reimplement order statistics, so it's probably not really worth the effort, since a manually coded treap seems to be much faster and doesn't require a lot more code.

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

    Is it possible to construct order-statistics tree quickly or use something like emplace_hint?

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

можна ли решать с помощью сия чуда эту и эту задачи? Если так подумать, то в роупе можно хранить какие-то структуры даных, например дерево отрезков, или же при изменениях порядка элементов будет полностью нарушаться корректность надстройки над частями контейнера?

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

Есть ли структура типа rope которая способна разворачивать подотрезок масива за O(logN)?

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

    Переворот на отрезке можно реализовать с помощью двух rope. В одной храним прямой массив, в другой — развернутый. Перевернуть — то же самое, что поменять соответствующие отрезки местами.

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

Правда ли, что эта штука персистентная?

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

What if we want to detach a block and set it anywhere else after reversing the block? Can we do this efficiently?

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

@ Perlik,Can I get the pdf link of "Effective STL"-by Meyers.

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

дополню информацию про erase, где аргументом выступает один индекс типа size_t. Решение было найдено после изучения этой функции внутри дерева. А именно:

// Erase, single character
  void
  erase(size_t __p)
  { erase(__p, __p + 1); }

думаю здесь все понятно: он убирает с позиции __p __p+1 элемент

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

very helpful

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

А есть что-то наподобие rope, которое может еще сортировать автоматически элементы?