我正致力于将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 ++实现它的最佳方法。
提前致谢!
答案 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的双射哈希时,还有另一种解决方案比两者都快:
如果存在一个小于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;
}