使用标准算法库实现二元关系的唯一等价

Using std algorithm library for unique equivalence with respect to binary relation

我在某些类型 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)

所有ab

对于类型为 T 的给定元素集合,我想删除除每个等价项 class 第一次出现以外的所有元素。我想出了以下代码,但一直在徘徊:

有没有没有显式循环的解决方案?

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,但我也会对其他教育选项感兴趣。

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());

先想出另一个loop版本,对比你自己的,统一就地,你可能会觉得有趣:

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_ifstd::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());

在这种情况下你需要 lambda,因为我们需要一个 unique-predicate,而等效的(在我的 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;
}

最后,重温第一个方案,重构为sub-concerns(忍不住):

// 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;
}

这是一种只有一个非常简单的循环的方法:

首先定义我们的 class,我将其称为 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;
}

Demo


编辑:正如 Richard Hodges 提到的,可以将任何擦除延迟到最后。虽然我无法让它看起来那么漂亮:

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;
}

Demo 2

你可以试试这个。这里的技巧是在谓词内部获取索引。

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::accumulate 扫描元素并将它们添加到累加器向量 xs(如果还没有) xs.

中的等价元素

有了这个,你根本不需要任何 hand-written 原始循环。

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);
}

I've modified AndyG's example code with my proposed function.

如上定义,std::accumulate调用push_back_if并复制累加器变量,return值再次move-assigned到累加器。这是非常低效的,但是可以通过改变push_back_if取一个引用来优化向量从而修改in-place。需要将初始值作为引用包装器传递给 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);
}

You can see in the example that the copy-constructor is almost completely eliminated.