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

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

Всем доброго времени суток!

Сегодня мне захотелось написать статью о таких зачастую важных на олимпиадах элементах STL, как map и set, то есть, о множествах. Понимаю, что практически все участники div. 1, которые пишут на С++ с ними знакомы и для них статья будет малополезной. Однако, как мне кажется, многие участники div. 2 напротив, могут о них не знать. В первую очередь на них и ориентирована данная статья. Также хочу отметить, что буду рад всякой критике, если Вы сочтёте её необходимой.

Итак, приступим. Как многие знают, "set" переводится с английского как "множество". Отсюда можно сделать логичный вывод, что set — это некая структура данных, которая реализует множество как математический объект. В С++11 есть две различные реализации множества. Первая пришла к нам из С++98. Это обычный set и основан он на реализации красно-чёрного дерева. Вторая реализация — unordered_set, которая была "узаконена" с введением стандарта С++11. Она основана на хеш-таблицах.

Для сета определены следующие полезные для нас операции:

1. Создание.

Как и для множество других контейнеров stl, чтобы объявить set, необходимо прописать

set<type_here> set_name_here;

Можно также использовать конструктор, чтобы сразу инициализировать контейнер. Инициализацию можно провести другим контейнером или парой итераторов [first, last).

2. Добавление.

Выполняется посредством перегруженной функции insert. Синтаксис её вызова:

set<*тип данных элементов, хранящихся в a*> a;
a.insert(*аргументы здесь*);

Вот её основные варианты:

pair<iterator,bool> insert( const value_type& value );

Принимает value — значение, которое следует вставить в множество и возвращает пару — итератор, указывающий на вставленный элемент и bool'еву переменную, обозначающую "успешность" вставки. Т.е. 1 — вставка произошла, 0 — нет. Данная функция выполняется за логарифм от размера контейнера, то есть, за O(logn).

iterator insert( iterator hint, const value_type& value );

Принимает значение value и итератор hint и пытается вставить элемент как можно ближе к итератору hint. Если вставка будет произведена сразу после или перед итератора hint, то функция отработает за O(1), иначе за O(logn).

template< class InputIt >
void insert( InputIt first, InputIt last );

Добавляет в set все элементы, находящиеся на промежутке [first;last) за O((last - first)logn). InputIt должен соответствовать требованиям InputIterator, т.е. поддерживать следующие операции:

  1. Сравнение (a!=b);
  2. Разыменование (*a);
  3. Инкремент (a++ и ++a);
  4. Инкремент значения (*a++);

При этом все указатели и итераторы на элементы set остаются в рабочем состоянии. Стоит также отметить, что set поддерживает лишь одно вхождение элемента с одинаковым значением, если же в множестве уже есть элемент с таким значением, он добавлен не будет. Однако, есть структура multiset, которая поддерживает множественные вхождения одного и того же элемента. Однако, операции над ней, за исключением небольших нюансов тождественны операциям над set и поэтому здесь она рассматриваться не будет.

3. Поиск элемента.

Существует несколько различных функций для поиска элементов в set. Вот они:

size_type count( const Key& key ) const;

Считает количество вхождений элемента с ключём key в контейнер. Очевидно, что в силу свойств контейнера, эта функция возвращает либо 0, либо 1. Работает за O(logn).

iterator find( const Key& key );

Находит элемент с ключём key. Если элемент найден, возвращает итератор на него, иначе на end() контейнера. Работает за O(logn).

iterator lower_bound( const Key& key );

Возвращает итератор на первый элемент в множестве, который больше, либо равен искомому. O(logn).

iterator upper_bound( const Key& key );

Возвращает итератор на первый элемент в множестве, который строго больше искомого. O(logn).

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

set<int> t;
lower_bound(t.begin(),t.end(),number); // Неправильно! Работает за О(n)
t.lower_bound(number); // Правильно!

4. Удаление.

Существует также перегруженная функция erase(), которая позволяет удалять элементы. Вот её основные варианты:

iterator erase( const_iterator position );

Удаляет элемент, находящийся в позиции, указанной итератором. Амортизированная сложность — O(1).

size_type erase( const key_type& key );

Удаляет все элементы с ключевым значением key. Понятно, что, если это просто set, а не multiset, кол-во таких элементов не превышает 1. Работает за O(logn + count(key)).

void erase( iterator first, iterator last );

Удаляет все элементы в диапазоне [first;last). Работает за O(logn + distance(first, last)).


Теперь поговорим о map. Название происходит от mapping — ассоциативный массив. Операции на нём практически тождественны set, однако в элементах контейнера хранится не одно значение, а пары ключ-значение. Сортировка в данном случае будет проходить таким же образом, как при обычной сортировке пар — вначале приоритетом идёт сравнение первых элементов в паре, потом, в случае их равности, сравниваются вторые элементы. Также стоит отметить весьма удобную реализацию обращения по индексам. Например, вот такой код:

map<string,int> test_map;
test_map["ten"]=10;
test_map["ten"]=8;

Вначале создаст в контейнере test_map пару ("ten",10), а затем изменит второй элемент в ней на 8.

unordered_set и unordered_map поддерживают большинство операций, поддерживаемых set и map, но, в среднем, выполняют их за О(1). Несмотря на это, их не всегда выгодно использовать, т.к. unordered означает неупорядоченный, т.е. элементы в них не поддерживаются в отсортированном порядке. В связи с этим, мы не сможем использовать такие контейнеры для, например, пирамидной сортировки, а также мы не сможем за О(1) получать доступ к наибольшему и наименьшему элементу, как в set и map. И, как следствие неупорядоченности, мы не сможем использовать на этих контейнерах операции lower_bound и upper_bound.

Напоследок могу предложить несколько задач, которые можно достаточно легко (по сравнению с решением без STL) решить с использованием этих структур данных:

  1. 1496 (Timus)
  2. 1196 (Timus)
  3. 1837 (Timus)
  4. Задача 13 (Codeforces, тренировка по мотивам UOI 2013)

Что ж, надеюсь, что моя статья была полезной кому-нибудь. С нетерпением жду ваших отзывов о ней :)

P.S. понимаю, что в статье материал раскрыт не полностью, хоть я и старался это сделать. Если после прочтения остались какие-то вопросы или замечания, можете их высказать.

Теги stl, set, map
  • Проголосовать: нравится
  • +42
  • Проголосовать: не нравится

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

lower_bound(t.begin(),t.end(),number); // Неправильно! Работает за О(n)

Я бы сказал, за O(N log N). Логарифм бинарного поиска плюс линия — доступ к элементу в сете в общем случае. Верно, нет?

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

    Неверно. set не умеет получать элемент по номеру. Если бы умел, кстати (RB-дерево это умеет с хранением дополнительной информации), то не за линию, а за log.

    А lower_bound для не-random access контейнеров работает за линию — просто пробегается по всем элементам при помощи итератора.

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

      Если асимптотика O(N), то lower_bound должен при вызове кешировать контейнер. Я сомневаюсь, что он так работает.

      "А lower_bound для не-random access контейнеров работает за линию — просто пробегается по всем элементам при помощи итератора." -- а факт поддержки метода можно проверять на шаблонах на этапе компиляции, и в соответствии с этим выбирать имплиментацию шаблона? Не покажите, а то я никогда такого не видел?

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

        Насколько я вижу код, в той реализации, что у меня [реализацию скрыл в правке] проверок не делается, но это все равно O(N) + O(log N) ибо каждый раз когда мы делаем len итераций, мы уменьшаем нашу длину пополам len/2, а длина суммарно уменьшится на O(N), то есть мы сделаем не больше 2 N операций лишних по сравнению с бинпоиском. Но это дольше, чем просто 1 раз пройти за линию, да.

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

        Зачем? Если не экономить количество сравнений, то можно просто пройти по всему контейнеру за линию.

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

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

Как использовать set, если там не готовые типы вроде int, double, а свои типы? Я слышал, что нужно писать свой компаратор, но никогда его связать с set'ом не получалось, можете описать как это делать правильно?

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

    Достаточно переопределить оператор < , так же, как и для сортировки.

    struct A {
       int x;
       
       bool operator < (const A &a) const {
         return x < A.x;
       }
    };
    
    map <A, int> h;
    A t = {1};
    h[t] = 2;
    
    • »
      »
      »
      11 лет назад, # ^ |
        Проголосовать: нравится +3 Проголосовать: не нравится

      А как это следует делать, если хочется использовать для хранения в set другой предикат, нежели тот, который используется для обычных сравнений?

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

        Какой например предикат?

        Можно написать

        bool operator < (const A &a) const {
             return x > A.x;
        }
        

        если Вы об этом.

        В любом случае, нужно ввести на множестве отношение порядка. А сравнения > и == выражаются через <, так что переопределяется только один оператор.

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

          Нет, я о том, чтобы использовать разные функции для обычного < и для хранения в set'e. Например, я хочу, чтобы у меня был set, в котором числа хранятся не по обычному порядку, а, скажем, лексикографически. Но при этом я не хочу перегружать оператор <, чтобы если я использовал его вне set, он бы работал как стандартный оператор <, а не сравнивающий лексикографически.

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

            Использовать второй аргумент шаблона std::set:

            struct CustomPredicate
            {
              bool operator()(const ValueType& v1, const ValueType& v2) const 
              {
                /*Custom compare method v1 vs v2*/
              }
            };
            
            typedef std::set<ValueType, CustomPredicate> CustomSetType;
            
            CustomSetType s;
            
      • »
        »
        »
        »
        11 лет назад, # ^ |
        Rev. 2   Проголосовать: нравится +5 Проголосовать: не нравится

        struct cmp { bool operator()(int a, int b) const { return a > b;} }; set<int, cmp> revOrder;

        Спасибо Avitella, добавил const

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

          Да, похоже, это именно то, что меня интересовало, спасибо =)

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

            Может не сработать. Если что, попробуй добавить const.

            struct cmp {
              bool operator()(const T& a, const T& b) const {
                // cmp
              }
            };
            
»
11 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

На счет задачи 13 помню сколько она шуму подняла на УОИ из-за того что ее очень легко можно было решить на С++, а на любой другой среде было трудно.

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

Возможно, тем, кому интересна, нова и полезна данная тема, будут также интересны и полезны простые задачи с informatics.mccme.ru с номерами от 3749 по 3769. (Кстати, буду благодарен, если кто-то скажет точно, чьи это задачи, и как к ним штатно добраться по ссылкам, а не по номерам.)

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

Думаю, было бы полезно добавить следующие "пользующиеся спросом" рецепты:
1) Правильное удаление части элементов из контейнера

for(p = begin; p != end; /*nothing*/) { if (ShouldDelete(*p)) { s.erase(*p++); } else { ++p; } }

2) Использование собственных типов в set/unordered_set в качестве ключа (т.е. когда требуется реализовать компаратор/хэшер).

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

    1) По-моему, это неправильное удаление части элементов из контейнера :) http://stackoverflow.com/questions/6438086/iterator-invalidation-rules

    Как минимум, если я не ошибаюсь, то p++ произойдет после erase, а итератор на erased-элемент является invalidated сразу после удаления(кажется, даже для всех контейнеров), поэтому у меня есть подозрение, что это UB.

    • можно написать что-то типа такого --- a.erase(std::remove_if(a.begin(), a.end(), ShouldDelete), a.end()) в качестве правильного удаления элементов из вектора(может быть и не только вектора).
    • »
      »
      »
      11 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      Приведенный мною способ правильный, потому что p++ возвращает значение, которое было перед инкрементом (в отличие от ++p). Способ придумал не я, это из книги Мейерса по STL.

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

        Действительно, ты прав.

        Не думал, что для set и map нет красивых и быстро работающих способов сделать это без использования итераторов.

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

    1) Почему не так?

    for(p = begin; p != end; /*nothing*/) { if (ShouldDelete(*p)) { p = s.erase(p); } else { ++p; } }
    

    Проверил, вроде работает, и выглядит лучше.

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

Мне было бы куда более интересно узнать различие между unordered_set/set. (unordered_map/map)

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

    unordered_X реализован на хеш-таблице, просто X на RB-дереве.

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

    Вкратце это описано. Как уже отметил предыдущий комментатор, unordered реализован на хеш-таблицах. В целом, unordered структуры поддерживают такие же операции, что и обычные, но за O(n) в худшем случае и О(1) амортизированно, за исключением того, что хранящиеся в них значения неупорядоченны, следовательно, их нельзя использовать в качестве приоритетной очереди или подобной структуры, которая выдавала бы min/max за О(1) или сортировала бы элементы.

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

А чтобы положить свой класс в unordered set/unordered map необходимо ли реализовать свою хэш функцию для класса? Если да, то можно пример привести?

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

    Да, надо для своего класса сделать специализацию шаблона std::hash. Проще всего вызвать std::hash для всех членов класса и как-то это скомбинировать. Пример можно найти здесь: http://en.cppreference.com/w/cpp/utility/hash

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

Большое спасибо, за то, что ты пишешь хорошие, полезные, статьи на ДВУХ языках. Мало кто так делает, а жалко. Такие посты очень сильно повышают наполненность полезной информацией Codeforces. Спасибо! =)

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

а как работают мультисет и мультимап? и как из них корректно удалять одиночные элементы?

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

    find-ом получить итератор, а потом вызвать erase

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

      Ага, тоже вчера на контесте это обнаружил:(

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

      Зашла в комментарии написать об этой возможности :) Конечно, перед удалением надо проверить, что итератор валиден.

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

В set<> все хранится в отсортированном порядке. A можно ли как нибудь узнать индекс элемента в нем ?

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

Is there any english translation of this blog ?

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

How do we access adjacent elements in a set? For e.g. if my number is x then I use *lower_bound(s.begin(), s.end(), x + 1) for the right bound, but how do I get the left bound?