【问题标题】:Using std algorithm library for unique equivalence with respect to binary relation使用 std 算法库实现关于二元关系的唯一等价
【发布时间】:2018-08-25 05:57:25
【问题描述】:

我在某些类型 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 类型的给定元素集合,我想删除每个等价类的第一次出现以外的所有元素。我想出了以下代码,但一直在徘徊:

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

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 不需要排序的人。出于 OP 的目的,它确实如此。 aabbccaa 之类的东西会变成 abca,而不是 abc,但后者是 OP 需要的。
  • 仅供参考,等价关系是可传递的,也就是说,equiv(a,b)&amp;&amp;equiv(b,c) 意味着 equiv(a,c)
  • @Elrond1337 您在下面的 cmets 中的示例不是传递性
  • 算法为什么要依赖我的等价关系的细节?我要问的是该算法适用于与operator&lt; 不可比较但与equivalentoperator== 是等价关系的类型。如果我错了并且它不是等价关系,那么代码就会被破坏,但这不是你的问题。

标签: c++ algorithm c++11 std


【解决方案1】:

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

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

【讨论】:

  • 不错。 +1。我认为您可以将实际擦除推迟到最后
  • 这肯定比我的解决方案好。
  • @RichardHodges 好点。已编辑。我就是想不出办法让它看起来更漂亮!
  • @AndyG 发布了一个答案,并提出了我将如何处理它的建议。我认为用迭代器来写它更惯用(也更漂亮)。
【解决方案2】:

在 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
  • "移到序列末尾" is not precise: "指向新逻辑端和范围物理端之间元素的迭代器仍然是可取消引用,但元素本身具有未指定的值(根据 MoveAssignable 后置条件)。” 除了这个小细节,很好的答案...
【解决方案3】:

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

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函数的结果进行过滤;或者,调用此函数需要 T 的一些 operator== 重载。但是,后者可能会与现有的重载发生冲突,或者以后阻止提供这样的重载(语义与 equivalent 不同)。
  • @Aconcagua 谢谢,只需编辑我的答案。一开始我以为operator==equivalent 可以是一样的。
  • 老实说,我自己的答案演变的方式完全相同......我什至将operator== 留在那里,但是,没有隐式使用。使用 explicit 进行更改的必要性是显而易见的(我认为至少是这样)...
【解决方案4】:

由于性能不是问题,您可以使用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);
}

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

如上所述,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);
}

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

【讨论】:

  • 我可以将 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 了解对标准的建议更改。
【解决方案5】:

先想出另一个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,因为我们需要一个唯一谓词,而等价(在我的 int 示例中由 operator== 表示)是一个二进制...

【讨论】:

    【解决方案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 可能会起作用。有趣。
    • 为什么S 中缺少return 的显式赋值运算符?
    • 这适用于您的类型S,它只有两个ints。出于参数的目的,假设我的类型是std::string,如果两个字符串包含相同的单词,equivalent 将返回 true。例如。 “我的第一个字符串”和“不是第一个字符串”是等价的,因为它们都包含单词“first”(单词=开头或结尾的子字符串或用空格分隔)。然后你的哈希函数需要将两个字符串映射到相同的值。
    • 当答案是两行难以理解的代码时,我投了反对票。
    • @Elrond1337 那将不是等价关系,您必须重新定义“唯一”的含义
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-11
    • 2018-11-08
    • 1970-01-01
    • 1970-01-01
    • 2012-11-24
    • 2021-04-14
    相关资源
    最近更新 更多