У меня есть бинарное отношение к некоторому типу 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 в том, что он применяет equivalent только к последовательным элементам. Подумайте о следующем вводе: (a, b, b, a) -> (a, b, a)
Любой, кто комментирует, что std::unique не нуждается в сортировке. Для целей OP это так. Что-то вроде aabbccaa станет abca, а не abc, но последнее - то, что нужно OP.
@ Elrond1337 Да, это правильно, я неправильно понял, вам нужно будет отсортировать их, чтобы они были последовательными.
@RichardHodges Дело не в производительности. Моя реализация - O(n^2), что нормально и действительно необходимо, учитывая спецификации. Мой вектор очень мал, поэтому приемлемо худшее время выполнения.
К вашему сведению, отношение эквивалентности транзитивно, то есть equiv(a,b)&&equiv(b,c) подразумевает equiv(a,c).
@JiveDadson Спасибо. В моем случае это действительно транзитивно.
@ Elrond1337 Ваш пример в комментариях ниже является транзитивным нет
Ой, извините, это верно. Мой пример был плохим. А уверяю вас, что в моей реальной проблеме (которую слишком сложно описать здесь, она есть). Итак, предположим, что это действительно отношение эквивалентности.
Почему алгоритм должен зависеть от деталей моего отношения эквивалентности? Все, что я прошу, это то, что алгоритм работает для типа, который не сопоставим с operator<, но с equivalent или operator==, что является отношением эквивалентности. Если я ошибаюсь и это не отношение эквивалентности, значит, код не работает, но это не ваша проблема.
Вероятно, у вас должен быть компаратор, совместимый с вашей функцией эквивалентности, тогда std::sort + std::unique выполнит эту работу.
@ Jarod42 Зависит ... Мне нравится ваш подход, но если вариант использования требует, чтобы порядок элементов (оставшихся) сохранялся, сортировка может его нарушить ...
@Aconcagua: вы все равно можете добавить дополнительный индекс для изменения порядка позже.
@Aconcagua: O(2 * n log(n)) - это все еще O(n log n), и из-за сложности мы выигрываем при любом n.
@ Jarod42 Я никогда не писал O(2n log(n)) ... Имеется в виду константа n0такое, что для любого n> n0 [...]. Ничего, забудем об этом ...





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, вероятно, подойдет. Интересно.
Почему в S отсутствует явный оператор присваивания, в котором отсутствует return?
Это работает для вашего типа S, который состоит всего из двух int. В качестве аргумента допустим, что мой тип - std::string, а equivalent возвращает истину, если обе строки содержат одно и то же слово. Например. «моя первая строка» и «не первая строка» будут эквивалентны, потому что оба они содержат слово «первая» (слово = подстрока в начале или в конце или разделены пробелами). Затем вашей хэш-функции необходимо сопоставить обе строки с одним и тем же значением.
Я проголосовал против, когда ответом были две непонятные строчки кода.
@ Elrond1337 Тогда это не будет отношением эквивалентности, и вам придется переопределить то, что вы подразумеваете под "уникальным".
Сначала придумываю другую версию петля, в отличие от вашей, она объединяет на месте, вы можете найти это интересным:
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
Вот способ, в котором есть только один очень простой цикл:
Сначала определите наш класс, который я назову 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;
}
красивый. +1. Я думаю, вы могли бы отложить фактическое стирание до конца
Это определенно лучше моего решения.
@RichardHodges Хорошее замечание. Отредактировано. Я просто не мог придумать, как сделать его красивее!
@AndyG опубликовал ответ с предложением, как я к нему подойду. Думаю, более идиоматично (и красиво) писать в терминах итераторов.
Вы можете попробовать это. Хитрость здесь в том, чтобы получить индекс внутри предиката.
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 Спасибо, просто отредактируйте мой ответ. Сначала я подумал, что operator== и equivalent могут быть одинаковыми.
Честно говоря, мой собственный ответ развивался точно так же ... Я даже оставил там operator==, но не неявно. При использовании явный необходимость изменений очевидна (по крайней мере, я так предполагаю) ...
Поскольку производительность не является проблемой, вы можете использовать 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 специально сказал, что он не заботится о производительности и хочет версию без рукописных циклов, поэтому версия с накоплением должна соответствовать требованиям.
@RichardHodges std::accumulate также должен быть реализован как acc = binary_op(std::move(acc), *i), поэтому содержимое вектора должно постоянно перемещаться, а не копироваться. И libstdC++, и clang реализуют накопление как __init = __binary_op(__init, *__first);, так что я думаю, это проблема качества реализации?
Я думаю, что std :: accumulate был разработан с учетом числовых типов. На самом деле его, вероятно, следует пересмотреть, чтобы эффективно обрабатывать сложные типы. В этом случае, вероятно, также следует переместить в <алгоритм>
@RichardHodges Изменение accumulate произошло совсем недавно, и его нет в стандарте C++ 17. Это объясняет, почему стандартные библиотеки еще не реализуют его.
См. wg21.link/p0616R0 для предлагаемых изменений стандарта.
Хм звучит как работа для тега std :: unique, edit: C++ 11, да, это вам не поможет.