C ++随机地从范围0:n-1(n> k)中取样k个数而无需替换

时间:2015-02-02 21:31:10

标签: c++ random

我正致力于将MATLAB仿真移植到C ++中。为此,我试图复制MATLAB' randsample() function。我还没有想出一个有效的方法来做到这一点。

所以我问你们所有人,如何在0 + n-1(n> k)的范围内随机抽样k数而不用C ++中的替换?

我考虑过以下伪代码(受到cppreference.com上第三个例子的启发),但我觉得它有点像hacky:

initialize vect<int> v of size n
for i = 0 to n-1
    v[i] = i
shuffle v
return v[0 to k-1]

这里的缺点也是首先需要构建一个大规模阵列。这似乎是缓慢/笨重的过度杀伤。

如果你能提供帮助,我会喜欢这里的方向。我对这个理论不太感兴趣(算法很有趣但与我现在的需求无关),而不是用C ++实现它的最佳方法。

提前致谢!

5 个答案:

答案 0 :(得分:7)

这是一种不需要生成和随机播放庞大列表的方法,以防N很大但k不是:

std::vector<int> pick(int N, int k) {
    std::random_device rd;
    std::mt19937 gen(rd());

    std::unordered_set<int> elems = pickSet(N, k, gen);

    // ok, now we have a set of k elements. but now
    // it's in a [unknown] deterministic order.
    // so we have to shuffle it:

    std::vector<int> result(elems.begin(), elems.end());
    std::shuffle(result.begin(), result.end(), gen);
    return result;
}

现在实施pickSet的天真方法是:

std::unordered_set<int> pickSet(int N, int k, std::mt19937& gen)
{
    std::uniform_int_distribution<> dis(1, N);
    std::unordered_set<int> elems;

    while (elems.size() < k) {
        elems.insert(dis(gen));
    }

    return elems;
}

但如果k相对于N较大,则此算法可能会导致大量冲突并且可能非常慢。我们可以做得更好,保证我们可以在每次插入时添加一个元素(由Robert Floyd提供给你):

std::unordered_set<int> pickSet(int N, int k, std::mt19937& gen)
{
    std::unordered_set<int> elems;
    for (int r = N - k; r < N; ++r) {
        int v = std::uniform_int_distribution<>(1, r)(gen);

        // there are two cases.
        // v is not in candidates ==> add it
        // v is in candidates ==> well, r is definitely not, because
        // this is the first iteration in the loop that we could've
        // picked something that big.

        if (!elems.insert(v).second) {
            elems.insert(r);
        }   
    }
    return elems;
}

答案 1 :(得分:4)

Bob Floyd创建了一个使用集合的随机样本算法。中间结构大小与您要采用的样本大小成比例。

它的工作原理是随机生成K个数字并将它们添加到一个集合中。如果生成的数字恰好存在于集合中,则会放置计数器的值,而不是保证尚未看到。因此,保证在线性时间内运行并且不需要大的中间结构。它仍具有相当好的随机分布属性。

这个代码基本上是从编程珍珠中解脱出来的,并进行了一些修改以使用更现代的C ++。

unordered_set<int> BobFloydAlgo(int sampleSize, int rangeUpperBound)
{
     unordered_set<int> sample;
     default_random_engine generator;

     for(int d = rangeUpperBound - sampleSize; d < rangeUpperBound; d++)
     {
           int t = uniform_int_distribution<>(0, d)(generator);
           if (sample.find(t) == sample.end() )
               sample.insert(t);
           else
               sample.insert(d);
     }
     return sample;
}

此代码尚未经过测试。

答案 2 :(得分:0)

从C ++ 17开始,有一个标准功能:<algorithm>库中的std::sample。保证具有线性时间复杂度。

样本 (用于双关语) 用法:

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

int main()
{
    std::vector<int> population {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::vector<int> sample;
    std::sample(population.begin(), population.end(), 
                std::back_inserter(sample),
                5,
                std::mt19937{std::random_device{}()});
    for(int i: sample)
        std::cout << i << " "; //prints 5 randomly chosen values from population vector

答案 3 :(得分:0)

鲍勃·弗洛伊德采样是一个很好的解决方案。 Reservoir sampling但是,当k与N处于相同的数量级时,可能是一个不错的选择。当我们可以创建大小为N的双射哈希时,还有另一种解决方案比两者都快:

enter image description here 如果存在一个小于N且与N互质的整数,则将该整数乘以一个双射。即新序列将是一个真实的排列,无需重复。一般情况可能很复杂,但是对于素数和二的幂,这很容易。

如果N是素数:

default_random_engine gen;
std::vector<size_t> samples(k);
std::uniform_int_distribution<size_t> distr(1, N);
const size_t a = distr(gen); // any number greater 0 is coprime with a prime
distr = std::uniform_int_distribution<size_t>(0, N);
const size_t b = distr(gen);
for (size_t i=0;i!=k;++i) samples[k]=i*a+b;
// sequence not random "enough", in case that matters
std::shuffle(samples.begin(),samples.end(),generator);
return samples;

如果N是2的幂(其他任何bijective hash函数也可以):

size_t constexpr xorshift(const size_t& n) {
  return n ^ (n>>std::numeric_limits<size_t>::digits);
}

vector<size_t> sample(const size_t& k,const size_t& N) {
  default_random_engine gen;
  std::vector<size_t> samples(k);
  std::uniform_int_distribution<size_t> distr;
  const size_t a = 2*distr(gen)+1; // uneven random constant
  const size_t b =   distr(gen);
  const size_t c = 2*distr(gen)+1; // uneven random constant
  // bijective hash
  for (size_t i=0;i!=k;++i) samples[k]=(xorshift(i*a+b)*c)&(N-1);
  // the samples are in random sequence too
  return samples;
}

储层采样:

vector<size_t> reservoir_sample(const size_t& k,const size_t& N) {
  vector<size_t> sample;
  if (k==0) return sample;
  std::default_random_engine gen;
  size_t i;
  for (i=0;i!=k;++i) sample.push_back(i);
  for (;i<N;++i) {
    uniform_int_distribution<size_t> distr(0,i);
    if (distr(gen) > k) continue;
    distr = uniform_int_distribution<size_t>(0,k-1);
    sample[distr(gen)]=i;
  }
  std::shuffle(sample.begin(),sample.end(),gen);
  return sample;
}

鲍勃·弗洛伊德采样:

std::unordered_set<size_t> floyd_sample(const size_t& k,const size_t& N) {
  std::default_random_engine gen;
  // for the benchmark I used a faster hash table
  std::unordered_set<size_t> elems(k); //preallocation is good
  for (size_t r = N - k; r < N; ++r) {
    size_t v = std::uniform_int_distribution<>(1, r)(gen);
    if (!elems.insert(v).second) elems.insert(r);
  }
  return elems;
}

对于任意大小n:

struct pow2hashround
  size_t m = 1; // multiplier
  // v = value to hash ; l = logarithm of size
  size_t inline operator()(
      const size_t v,
      const size_t l=numeric_limits<size_t>::digits) const 
  {
    size_t h = v;
    h*=m;
    h&=(~size_t(0))>>(numeric_limits<size_t>::digits-l);
    h^=h>>(l/2);
    return h;
  }
};

template<size_t nrounds = 5>
struct arbitrary_bijective_hash{
  size_t n = 0;
  pow2hashround rounds[nrounds];
  size_t offsets[nrounds];
  template<class generator>
  arbitrary_bijective_hash(const size_t& n,generator& gen) : n(n)
  {
    const size_t next_pow2 = size_t(1)<<log2(n);
    uniform_int_distribution<size_t> distr(n/2);
    uniform_int_distribution<size_t> distrl(2*n/nrounds,n);
    for (size_t i=0;i!=nrounds;++i) {
      rounds[i].m = 2*distr(gen)+1;
      offsets[i] = distrl(gen);
    }
  }
  size_t inline operator()(const size_t& v){
    const size_t next_pow2 = size_t(1)<<log2(n);
    size_t hash = v;
    for (size_t i=0;i!=nrounds;++i) {
      if (hash<next_pow2) hash = rounds[i](hash,log2(n));
      hash += offsets[i];
      if (hash<offsets[i]) hash-=n; // integer overflow
      else if (hash>=n) hash-=n; // simplified modulo reduction
    }
    return hash;
  }
};

vector<size_t> sample(const size_t& k,const size_t& n) {
  std::default_random_engine gen;
  std::vector<size_t> samples(k);
  arbitrary_bijective_hash hash(n,gen);
  for (size_t i=0;i!=k;++i) samples[i]=hash(i);
  return samples;
}

一些注意事项:std :: shuffle始终会随机整理整个范围,但是当您只需要k个项目时,可以在第k个元素处使用fisher-yates shuffle停止操作,从而使其与基于hash的变体几乎一样快要从中采样的元素已经存在于内存中,您可以对其进行修改。 如果事先不知道数字n或只能顺序读取要采样的项目,则应该使用Algorithm L,这是最佳选择。

答案 4 :(得分:0)

所以这是我想出的一个解决方案,它将以随机顺序生成样本,而不是以确定性的方式生成样本,以后需要将其改组:

vector<int> GenerateRandomSample(int range, int samples) {
  vector<int> solution; // Populated in the order that the numbers are generated in.
  vector<int> to_exclude; // Inserted into in sorted order.
  for(int i = 0; i < samples; ++i) {
    auto raw_rand = rand() % (range - to_exclude.size());
    // This part can be optimized as a binary search
    int offset = 0;
    while(offset < to_exclude.size() &&
        (raw_rand+offset) >= to_exclude[offset]) {
      ++offset;
    }
    // Alternatively substitute Binary Search to avoid linearly
    // searching for where to put the new element. Arguably not
    // actually a benefit.
    // int offset = ModifiedBinarySearch(to_exclude, raw_rand);

    int to_insert = (raw_rand + offset);
    to_exclude.insert(to_exclude.begin() + offset, to_insert);
    solution.push_back(to_insert);
  }  
  return solution;
}

我添加了一个可选的二进制搜索来查找新插入位置 生成随机成员,但是在尝试对大范围(N)/和集合(K)进行基准测试(在codeinterview.io/上完成)之后,我发现这样做并没有任何明显的好处,仅是线性遍历和提前退出

编辑:经过进一步的广泛测试,我发现了足够大的参数:(例如N = 1000,K = 500,TRIALS = 10000) 实际上,二进制搜索方法确实提供了很大的改进: 对于给定的参数: 二进制搜索:〜2.7秒 线性:〜5.1秒 确定性的(不按Barry在基于Robert Floyd的公认答案中提出的改组):约3.8秒

int ModifiedBinarySearch(const vector<int>& collection, int raw_rand) {
  int offset = 0;
  int beg = 0, end = collection.size() - 1;
  bool upper_range = 0;
  while (beg <= end) {
    offset = (beg + end) / 2;
    auto to_search_for = (raw_rand+offset);
    auto left = collection[offset];
    auto right = (offset+1 < collection.size() ?
        collection[offset+1] :
        collection[collection.size() - 1]);
    if ((raw_rand+offset) < left) {
      upper_range = false;
      end = offset - 1;
    } else if ((raw_rand+offset+1) >= right) {
      upper_range = true;
      beg = offset + 1;
    } else {
      upper_range = true;
      break;
    }
  }
  offset = ((beg + end) / 2)  + (upper_range ? 1 : 0);
  return offset;
}