Использование библиотеки алгоритмов std для уникальной эквивалентности по отношению к бинарному отношению

У меня есть бинарное отношение к некоторому типу T, вызванное функцией equivalent:

bool equivalent(T const& a, T const& b); // returns true if a and b are equivalent

Он обладает свойствами, которые

equivalent(a, a) == true

а также

equivalent(a, b) == equivalent(b, a)

для всех a, b.

Для данной коллекции элементов типа T я хочу удалить все, кроме первого вхождения каждого класса эквивалентности. Я придумал следующий Код, но блуждал:

Есть ли решение без явного цикла?

std::vector<T> filter_all_but_one_for_each_set_of_equivalent_T(std::vector<T> const& ts) {
  std::vector<T> result;
  for (auto iter = ts.begin(); iter != ts.end(); ++iter) {
     auto const& elem = *iter;
     bool has_equivalent_element_at_earlier_position = std::any_of(
        ts.begin(),
        iter,
        &equivalent
     );
     if (not has_equivalent_element_at_earlier_position) {
        result.push_back(routing_pin);
     }
  }
  return result;
}

Обновлять

Насколько я понимаю, std::unique не подходит, потому что мой тип T не поддается сортировке. И потому что в моем случае у меня только C++ 11, но мне были бы интересны и другие варианты обучения.

Хм звучит как работа для тега std :: unique, edit: C++ 11, да, это вам не поможет.

arynaq 16.03.2018 09:30
std :: unique - однако это сделало бы элементы все уникальными, а не только конкретными ...
Aconcagua 16.03.2018 09:35

Я хочу сделать элементы все уникальными. Однако их нельзя отсортировать.

Elrond1337 16.03.2018 09:39

Верно, но что более важно: «Удаляет все, кроме первого элемента, из каждого последовательная группа эквивалентных элементов из диапазона ...» В моем случае эквивалентные элементы распределены по всей коллекции, поэтому вы должны отсортировать их заранее, если сможете.

Elrond1337 16.03.2018 09:46

насколько велик вектор?

Richard Hodges 16.03.2018 09:54

Проблема с std::unique в том, что он применяет equivalent только к последовательным элементам. Подумайте о следующем вводе: (a, b, b, a) -> (a, b, a)

Elrond1337 16.03.2018 09:54

Любой, кто комментирует, что std::unique не нуждается в сортировке. Для целей OP это так. Что-то вроде aabbccaa станет abca, а не abc, но последнее - то, что нужно OP.

Max Vollmer 16.03.2018 09:55

@ Elrond1337 Да, это правильно, я неправильно понял, вам нужно будет отсортировать их, чтобы они были последовательными.

Samer Tufail 16.03.2018 09:56

@RichardHodges Дело не в производительности. Моя реализация - O(n^2), что нормально и действительно необходимо, учитывая спецификации. Мой вектор очень мал, поэтому приемлемо худшее время выполнения.

Elrond1337 16.03.2018 09:56

К вашему сведению, отношение эквивалентности транзитивно, то есть equiv(a,b)&&equiv(b,c) подразумевает equiv(a,c).

Jive Dadson 16.03.2018 10:02

@JiveDadson Спасибо. В моем случае это действительно транзитивно.

Elrond1337 16.03.2018 10:05

@ Elrond1337 Ваш пример в комментариях ниже является транзитивным нет

Passer By 16.03.2018 10:06

Ой, извините, это верно. Мой пример был плохим. А уверяю вас, что в моей реальной проблеме (которую слишком сложно описать здесь, она есть). Итак, предположим, что это действительно отношение эквивалентности.

Elrond1337 16.03.2018 10:09
"Слишком сложно описать здесь" Извините, но это пахнет.
Max Vollmer 16.03.2018 10:10

Почему алгоритм должен зависеть от деталей моего отношения эквивалентности? Все, что я прошу, это то, что алгоритм работает для типа, который не сопоставим с operator<, но с equivalent или operator==, что является отношением эквивалентности. Если я ошибаюсь и это не отношение эквивалентности, значит, код не работает, но это не ваша проблема.

Elrond1337 16.03.2018 10:14

Вероятно, у вас должен быть компаратор, совместимый с вашей функцией эквивалентности, тогда std::sort + std::unique выполнит эту работу.

Jarod42 16.03.2018 10:42

@ Jarod42 Зависит ... Мне нравится ваш подход, но если вариант использования требует, чтобы порядок элементов (оставшихся) сохранялся, сортировка может его нарушить ...

Aconcagua 16.03.2018 13:10

@Aconcagua: вы все равно можете добавить дополнительный индекс для изменения порядка позже.

Jarod42 16.03.2018 13:38

@Aconcagua: O(2 * n log(n)) - это все еще O(n log n), и из-за сложности мы выигрываем при любом n.

Jarod42 16.03.2018 13:56

@ Jarod42 Я никогда не писал O(2n log(n)) ... Имеется в виду константа n0такое, что для любого n> n0 [...]. Ничего, забудем об этом ...

Aconcagua 16.03.2018 14:16
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
20
168
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

struct S {
    int eq;
    int value;
    bool operator==(const S& other) const { return eq == other.eq; }
};

namespace std {
    template <> struct hash<S>
    {
        size_t operator()(const S &s) const
        {
            return hash<int>()(s.eq);
        }
    };
}

array<S, 6> as{ { {1,0},{2,0},{3,0},{ 1,1 },{ 2,1 },{ 3,1 } } };
unordered_set<S> us(as.cbegin(), as.cend());

Какую хеш-функцию вы используете для unordered_set? Постоянная функция hash(t) == 0, вероятно, подойдет. Интересно.

Elrond1337 16.03.2018 09:59

Почему в S отсутствует явный оператор присваивания, в котором отсутствует return?

Zereges 16.03.2018 10:00

Это работает для вашего типа S, который состоит всего из двух int. В качестве аргумента допустим, что мой тип - std::string, а equivalent возвращает истину, если обе строки содержат одно и то же слово. Например. «моя первая строка» и «не первая строка» будут эквивалентны, потому что оба они содержат слово «первая» (слово = подстрока в начале или в конце или разделены пробелами). Затем вашей хэш-функции необходимо сопоставить обе строки с одним и тем же значением.

Elrond1337 16.03.2018 10:03

Я проголосовал против, когда ответом были две непонятные строчки кода.

Jive Dadson 16.03.2018 10:04

@ Elrond1337 Тогда это не будет отношением эквивалентности, и вам придется переопределить то, что вы подразумеваете под "уникальным".

Passer By 16.03.2018 10:05

Сначала придумываю другую версию петля, в отличие от вашей, она объединяет на месте, вы можете найти это интересным:

std::vector<int> v({1, 7, 1, 8, 9, 8, 9, 1, 1, 7});

auto retained = v.begin();
for(auto i = v.begin(); i != v.end(); ++i)
{
    bool isFirst = true;
    for(auto j = v.begin(); j != retained; ++j)
    {
        if (*i == *j)
        {
            isFirst = false;
            break;
        }
    }

    if (isFirst)
    {
        *retained++ = *i;
    }
}
v.erase(retained, v.end());

Это была база для версии, использующей std::remove_if и std::find_if:

auto retained = v.begin();
auto c = [&v, &retained](int n)
        {
            if (std::find_if (v.begin(), retained, [n](int m) { return m == n; }) != retained)
                return true;
            // element remains, so we need to increase!!!
            ++retained;
            return false;
        };
v.erase(std::remove_if (v.begin(), v.end(), c), v.end());

В этом случае вам понадобится лямбда, так как нам нужен уникальный предикат, тогда как эквивалент (в моем примере int, представленный operator==) является двоичным ...

Расширяя мой комментарий в ответе AndyG:

template<class T, class A, class Equivalent>
auto deduplicated2(std::vector<T, A> vec, Equivalent&& equivalent) -> std::vector<T, A>
{
    auto current = std::begin(vec);

    // current 'last of retained sequence'
    auto last = std::end(vec);

    while (current != last)
    {
        // define a predicate which checks for equivalence to current
        auto same = [&](T const& x) -> bool
        {
            return equivalent(*current, x);
        };

        // move non-equivalent items to end of sequence
        // return new 'end of valid sequence'
        last = std::remove_if (std::next(current), last, same);
    }
    // erase all items beyond the 'end of valid sequence'
    vec.erase(last, std::end(vec));
    return vec;
}

Кредит AndyG, пожалуйста.

Для очень больших векторов, где T является хешируемым, мы можем стремиться к решению O (n):

template<class T, class A, class Equivalent>
auto deduplicated(std::vector<T, A> const& vec, Equivalent&& equivalent) -> std::vector<T, A>
{
    auto seen = std::unordered_set<T, std::hash<T>, Equivalent>(vec.size(), std::hash<T>(), std::forward<Equivalent>(equivalent));

    auto result = std::vector<T, A>();
    result.resize(vec.size());

    auto current = std::begin(vec);
    while (current != std::end(vec))
    {
        if (seen.insert(*current).second)
        {
            result.push_back(*current);
        }
    }
    return result;
}

Наконец, пересмотр первого решения и рефакторинг на подзадачи (я ничего не могу с собой поделать):

// in-place de-duplication of sequence, similar interface to remove_if
template<class Iter, class Equivalent>
Iter inplace_deduplicate_sequence(Iter first, Iter last, Equivalent&& equivalent)
{
    while (first != last)
    {
        // define a predicate which checks for equivalence to current
        using value_type = typename std::iterator_traits<Iter>::value_type;
        auto same = [&](value_type const& x) -> bool
        {
            return equivalent(*first, x);
        };

        // move non-equivalent items to end of sequence
        // return new 'end of valid sequence'
        last = std::remove_if (std::next(first), last, same);
    }
    return last;
}

// in-place de-duplication on while vector, including container truncation    
template<class T, class A, class Equivalent>
void inplace_deduplicate(std::vector<T, A>& vec, Equivalent&& equivalent)
{
    vec.erase(inplace_deduplicate_sequence(vec.begin(), 
                                           vec.end(), 
                                           std::forward<Equivalent>(equivalent)), 
              vec.end());
}

// non-destructive version   
template<class T, class A, class Equivalent>
auto deduplicated2(std::vector<T, A> vec, Equivalent&& equivalent) -> std::vector<T, A>
{
    inplace_deduplicate(vec, std::forward<Equivalent>(equivalent));
    return vec;
}

Хорошее использование std::next

AndyG 16.03.2018 10:30
"перейти в конец последовательности" - это не точно: «Итераторы, указывающие на элемент между новым логическим концом и физическим концом диапазона, по-прежнему могут быть разыменованы, но сами элементы имеют неопределенные значения (согласно постусловию MoveAssignable)». Помимо этой маленькой детали, хороший ответ ...
Aconcagua 16.03.2018 13:24

Вот способ, в котором есть только один очень простой цикл:

Сначала определите наш класс, который я назову A вместо T, потому что T обычно используется для шаблонов:

class A{
public:
    explicit A(int _i) : i(_i){};
    int get() const{return i;}
private:
    int i;
};

А затем наша функция equivalent просто сравнивает целые числа на предмет равенства:

bool equivalent(A const& a, A const& b){return a.get() == b.get();}

Далее я определю функцию фильтрации.

Идея здесь состоит в том, чтобы воспользоваться преимуществом std::remove, чтобы эффективно выполнять цикл и стирание для нас (обычно он меняет местами элементы до конца, чтобы вы не смещали вектор для каждого удаления).

Мы начинаем с удаления всего, что соответствует первому элементу, затем удаляем все, что соответствует второму элементу (что теперь гарантировано! = Первому элементу) и так далее.

std::vector<A> filter_all_but_one_for_each_set_of_equivalent_A(std::vector<A> as) {
    for(size_t i = 1; i < as.size(); ++i){
       as.erase(std::remove_if (as.begin() + i, as.end(), [&as, i](const A& next){return equivalent(as[i-1], next);}), as.end());
    }
    return as;
}

Демо


Обновлено: как сказал Ричард Ходжес, можно отложить любое стирание до самого конца. Хотя я не мог сделать это так красиво:

std::vector<A> filter_all_but_one_for_each_set_of_equivalent_A(std::vector<A> as) {
    auto end = as.end();
    for(size_t i = 1; i < std::distance(as.begin(), end); ++i){
       end = std::remove_if (as.begin() + i, end, [&as, i](const A& next){return equivalent(as[i-1], next);});
    }
    as.erase(end, as.end());
    return as;
}

Демо 2

красивый. +1. Я думаю, вы могли бы отложить фактическое стирание до конца

Richard Hodges 16.03.2018 10:17

Это определенно лучше моего решения.

Elrond1337 16.03.2018 10:19

@RichardHodges Хорошее замечание. Отредактировано. Я просто не мог придумать, как сделать его красивее!

AndyG 16.03.2018 10:27

@AndyG опубликовал ответ с предложением, как я к нему подойду. Думаю, более идиоматично (и красиво) писать в терминах итераторов.

Richard Hodges 16.03.2018 10:29

Вы можете попробовать это. Хитрость здесь в том, чтобы получить индекс внутри предиката.

std::vector<T> output; 
std::copy_if (
    input.begin(), input.end(),
    std::back_inserter(output),
    [&](const T& x) {
        size_t index = &x - &input[0];
        return find_if (
            input.begin(), input.begin() + index, x,
            [&x](const T& y) {
                return equivalent(x, y);
            }) == input.begin() + index;
    });

Однако вместо этого вам придется использовать std::find_if, поскольку элементы должны фильтроваться в соответствии с результатом функции equivalent; в качестве альтернативы для вызова этой функции требуется некоторая перегрузка operator== для T. Однако последнее может конфликтовать с существующей перегрузкой или препятствовать предоставлению такой перегрузки (с другой семантикой, чем equivalent) позже.

Aconcagua 16.03.2018 10:49

@Aconcagua Спасибо, просто отредактируйте мой ответ. Сначала я подумал, что operator== и equivalent могут быть одинаковыми.

hgminh 16.03.2018 11:02

Честно говоря, мой собственный ответ развивался точно так же ... Я даже оставил там operator==, но не неявно. При использовании явный необходимость изменений очевидна (по крайней мере, я так предполагаю) ...

Aconcagua 16.03.2018 13:04
Ответ принят как подходящий

Поскольку производительность не является проблемой, вы можете использовать std::accumulate для сканирования элементов и добавления их в вектор аккумулятора xs, если это еще не сделано. эквивалентный элемент в xs.

При этом вам вообще не нужны рукописные необработанные циклы.

std::vector<A> filter_all_but_one_for_each_set_of_equivalent_A(std::vector<A> as) {       
    return std::accumulate(as.begin(), as.end(), 
                           std::vector<A>{}, [](std::vector<A> xs, A const& x) {
                               if ( std::find_if (xs.begin(), xs.end(), [x](A const& y) {return equivalent(x,y);}) == xs.end() ) {
                                   xs.push_back(x);
                               }

                               return xs;
                           });
}

С двумя вспомогательными функциями это становится действительно читаемым:

bool contains_equivalent(std::vector<A> const& xs, A const& x) {
    return std::find_if (xs.begin(), xs.end(), 
                        [x](A const& y) {return equivalent(x,y);}) != xs.end();
};

std::vector<A> push_back_if (std::vector<A> xs, A const& x) {
        if ( !contains_equivalent(xs, x) ) {
            xs.push_back(x);
        }

        return xs;
    };

Сама функция - это просто вызов std::accumulate:

std::vector<A> filter_all_but_one_for_each_set_of_equivalent_A(std::vector<A> as) {       
    return std::accumulate(as.begin(), as.end(), std::vector<A>{}, push_back_if);
}

Я изменил пример кода AndyG предложенной функцией.

Как определено выше, std::accumulate вызывает push_back_if с копией аккумуляторной переменной, и возвращаемое значение снова присваивается аккумулятору перемещением. Это очень неэффективно, но его можно оптимизировать, изменив push_back_if так, чтобы он принимал ссылку, чтобы вектор изменялся на месте. Начальное значение необходимо передать как обертку ссылки с std::ref, чтобы исключить оставшиеся копии.

std::vector<A>& push_back_if (std::vector<A>& xs, A const& x) {
        if ( !contains_equivalent(xs, x) ) {
            xs.push_back(x);
        }

        return xs;
    };

std::vector<A> filter_all_but_one_for_each_set_of_equivalent_A(std::vector<A> const& as) {       
    std::vector<A> acc;
    return std::accumulate(as.begin(), as.end(), std::ref(acc), push_back_if);
}

В примере видно, что конструктор копирования практически полностью исключен.

Я могу изменить push_back_if, чтобы принимать xs по ссылке и возвращать ссылку, а также передавать аккумулятор как std::ref(a) локальному объекту a. Это должно избавить почти от всех копий. Он компилирует и печатает правильные результаты (wandbox.org/permlink/vt6QHIG37iY9Wh04), и я думаю, что он должен соответствовать определению накопления. Ограничений на переданную функцию не так много. Но OP специально сказал, что он не заботится о производительности и хочет версию без рукописных циклов, поэтому версия с накоплением должна соответствовать требованиям.

Jens 17.03.2018 22:16

@RichardHodges std::accumulate также должен быть реализован как acc = binary_op(std::move(acc), *i), поэтому содержимое вектора должно постоянно перемещаться, а не копироваться. И libstdC++, и clang реализуют накопление как __init = __binary_op(__init, *__first);, так что я думаю, это проблема качества реализации?

Jens 17.03.2018 22:34

Я думаю, что std :: accumulate был разработан с учетом числовых типов. На самом деле его, вероятно, следует пересмотреть, чтобы эффективно обрабатывать сложные типы. В этом случае, вероятно, также следует переместить в <алгоритм>

Richard Hodges 17.03.2018 23:08

@RichardHodges Изменение accumulate произошло совсем недавно, и его нет в стандарте C++ 17. Это объясняет, почему стандартные библиотеки еще не реализуют его.

Jens 18.03.2018 10:46

См. wg21.link/p0616R0 для предлагаемых изменений стандарта.

Jens 18.03.2018 13:27

Другие вопросы по теме