【问题标题】:Can the order of std::shuffle depend on anything but the RNG?std::shuffle 的顺序可以依赖于 RNG 以外的任何东西吗?
【发布时间】:2021-01-08 17:11:39
【问题描述】:

为了以相同的顺序打乱两个向量,很容易做类似的事情

whatever_rng_type rng2(rng1);

std::shuffle(vec1.begin(), vec1.end(), rng1);
std::shuffle(vec2.begin(), vec2.end(), rng2);

其中两个随机播放使用相同的 RNG 状态。但是,在我检查的standard draft 中,我没有看到这些洗牌实际上产生相同顺序的任何要求。

std::shuffle 必须使用提供的 RNG 作为随机源,但实现也可能会针对不同的元素大小采用不同的代码路径。也许实现可能对某些类型使用 AVX512 收集/分散指令,而对其他类型使用通用的非矢量化代码路径,这可能会影响结果排序。

使用相同的种子执行两次洗牌实际上是获得相同订单的安全方法吗?我在以后的标准版本中遗漏了什么,或者缺陷报告之类的吗?

【问题讨论】:

  • FWIW,假设向量具有相同的长度,您可以考虑用整数填充一个向量 0 .. vec1.size() - 1,将其改组,并使用它来索引 vec1 和 @987654327 @.
  • @TonyDelroy:是的,但有时您确实需要对向量进行物理重新排序(与直接洗牌相比,使用洗牌索引向量对它们进行物理重新排序既昂贵又尴尬)。我真正想要的是某种由原始两个向量支持的pair_thing(vec1, vec2),其中对这对事物的洗牌会自动将原始向量洗牌在一起。
  • 我仔细检查了标准并同意没有什么要求shuffle 对其使用随机数做出任何保证。它只是简单地说明了“在此函数的实现使用随机数的范围内,g 引用的对象应作为实现的随机源。”。因此,剩下的问题似乎是您是否对任何特定实现的实现定义或观察行为感兴趣,或者是否会坚持符合标准的可移植解决方案(例如,改组代理对象或使用索引数组)...?
  • 您的问题询问标准保证了什么,但您的评论说您正在寻找解决方案。这些是不同的问题,导致下面的混乱。如果您确实想同时提出这两个问题,请编辑问题。另外,您对图书馆解决方案还满意吗? boost,range-v3 可以让你用几行代码就能得到你想要的效果,而且效率很高。
  • @user2357112supportsMonica 不确定我是否同意它效率低下 - 有 O(n) 算法用于重新排序,因此它的效率与排序两次一样渐近(但需要 O(n) 额外存储空间)。

标签: c++ random language-lawyer


【解决方案1】:

我仔细检查了标准并同意没有任何要求 shuffle 对其使用随机数做出任何保证。它只是说:

“在此函数的实现使用随机数的范围内,g 引用的对象应作为实现的随机源。”。

所以,剩下的问题似乎是您是否对 任何特定的实现定义或观察到的行为 实施(S),或将坚持符合标准的便携式 解决方案(例如,改组代理对象或使用数组 指数)...?

根据您的 cmets,您似乎反对索引数组的建议,因此在下面 - 使用自定义迭代器和代理对向量本身进行洗牌的实现...

(做的不是很仔细——更多的是概念证明/插图,所以在使用任何重要的东西之前请仔细检查......)

该方法需要一个 move_together 对象来保留对向量的引用,然后传递 shuffle iterators,该对象具有指向 move_together 对象的指针和正在处理的向量中的索引。您可以通过前面提到 move_together 对象并在迭代器中直接使用指向两个向量的指针或引用来简化这一点。当迭代器被取消引用时,它们会返回支持swapping 的代理对象。

它表面上适用于 GCC 10.2 和 clang 10,但对于另一个需要更完整的迭代器或代理的编译器,std::shuffle 可能有不同的实现......

#include <iostream>
#include <vector>
#include <random>
#include <string>
#include <algorithm>

template <typename T1, typename T2>
struct move_together;

template <typename T1, typename T2>
struct proxy
{
    const move_together<T1, T2>* p_;
    const size_t i_;
    proxy& operator=(const proxy& rhs);
};

template <typename T1, typename T2>
struct move_together
{
    move_together(std::vector<T1>& v1, std::vector<T2>& v2)
      : v1_(v1), v2_(v2)
    { }
    struct iterator
    {
        using iterator_category = std::random_access_iterator_tag;
        using difference_type = ssize_t;
        using value_type = proxy<T1, T2>;
        using pointer = value_type*;
        using reference = value_type&;

        const move_together* p_;
        size_t i_;
        value_type operator*() { return {p_, i_}; }
        bool operator==(const iterator& rhs) const { return i_ == rhs.i_; }
        bool operator!=(const iterator& rhs) const { return !(*this == rhs); }
        difference_type operator-(const iterator& rhs) const
           { return i_ - rhs.i_; }
        iterator operator+(int distance) const
           { return {p_, i_ + distance}; }
        iterator operator++(int) { auto x = *this; ++i_; return x; }
        iterator& operator++() { ++i_; return *this; }
    };

    iterator begin() { return {this, 0}; }
    iterator end()   { return {this, std::min(v1_.size(), v2_.size())}; }
    std::vector<T1>& v1_;
    std::vector<T2>& v2_;
};

template <typename T1, typename T2>
proxy<T1, T2>& proxy<T1, T2>::operator=(const proxy<T1, T2>& rhs)
{
    p_->v1_[i_] = rhs.p_->v1_[rhs.i_];
    p_->v2_[i_] = rhs.p_->v2_[rhs.i_];
}

template <typename T1, typename T2>
void swap(proxy<T1, T2> lhs, proxy<T1, T2> rhs) {
    using std::swap;
    swap(lhs.p_->v1_[lhs.i_], rhs.p_->v1_[rhs.i_]);
    swap(lhs.p_->v2_[lhs.i_], rhs.p_->v2_[rhs.i_]);
}

int main()
{
    std::vector<int> v1{ {1, 2, 3, 4, 5} };
    std::vector<std::string> v2{ {"one", "two", "three", "four", "five"} };

    std::random_device rd;
    std::mt19937 rng{rd()};

    move_together m{v1, v2};
    std::shuffle(m.begin(), m.end(), rng);

    for (const auto& x : v1) std::cout << x << '/';
    std::cout << '\n';
    for (const auto& x : v2) std::cout << x << '/';
    std::cout << '\n';
}

【讨论】:

  • 这似乎是对另一个问题的回答。
  • @Barmar 似乎在回答 this request
  • 我已确认 OP 的理解,即该标准不保证他的方法将在 cmets 中工作(但会在此处复制/粘贴我的评论以获得良好的衡量标准),这为他的功能需求提供了一个实现在 O(1) 空间中,没有笨拙的第三个索引向量,以后需要使用。
猜你喜欢
  • 2014-12-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-22
  • 1970-01-01
  • 1970-01-01
  • 2019-09-25
  • 2016-04-07
相关资源
最近更新 更多