C++ 从 0:n-1 (n > k) 范围内随机抽取 k 个数,不放回
C++ randomly sample k numbers from range 0:n-1 (n > k) without replacement
我正在努力将 MATLAB 模拟移植到 C++ 中。为此,我试图复制 MATLAB 的 randsample() function。我还没有想出一个有效的方法来做到这一点。
所以我问大家,在 C++ 中,如何最好地从范围 0:n-1(对于 n > k)中随机抽取 k 个数字而不进行替换?
我考虑过以下伪代码(灵感来自 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]
这里的缺点也是需要先构建一个庞大的数组。这似乎 slow/clunky 矫枉过正。
如果你能帮忙的话,我很乐意在这里提供一些指导。我对理论不太感兴趣(算法很有趣,但现在与我的需求无关),而不是用 C++ 实现它的最佳方法。
提前致谢!
Bob Floyd 创建了一个使用集合的随机样本算法。中间结构大小与您要获取的样本大小成正比。
它的工作原理是随机生成 K 个数字并将它们添加到一个集合中。如果生成的数字碰巧已经存在于集合中,它会放置一个计数器的值,而不是保证还没有看到。从而保证在线性时间内运行,不需要很大的中间结构。它仍然具有很好的随机分布特性。
此代码基本上是从 Programming Pearls 中提取的,并进行了一些修改以使用更现代的 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;
}
此代码尚未经过测试。
这是一种不需要生成和洗牌巨大列表的方法,以防 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;
}
从 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
Bob Floyds 采样是一个很好的解决方案。 Reservoir sampling 然而,当 k 与 N 处于同一数量级时,这可能是一个不错的选择。
水库采样:
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;
}
随机抽样不完整:
#include <vector>
#include <random>
#include <algorithm>
#include <chrono>
#include <iostream>
#include <iomanip>
using std::vector;
using std::uniform_int_distribution;
using std::shuffle;
using std::cout;
using std::swap;
template<class iterator,class generator>
void inline shuffle(iterator beg,iterator unt,iterator end,generator gen){
const size_t n = end-beg;
for (;beg!=unt;++beg) {
size_t i=end-beg;
size_t r=uniform_int_distribution<size_t>(0,i)(gen);
swap(*beg,*(beg+r));
}
}
template<class iterator>
vector<size_t> sample(const size_t& k,iterator beg,iterator end) {
vector<size_t> sample(k);
std::default_random_engine gen;
if (k<(end-beg)/2) {
shuffle(beg,beg+k,end,gen);
for (size_t i=0;i!=k;(++i,++beg)) sample[i] = *beg;
} else {
const size_t l = end-beg-k;
shuffle(beg,beg+l,end,gen);
beg+=l;
for (size_t i=0;i!=k;(++i,++beg)) sample[i] = *beg;
}
return sample;
}
int main(int argc,char** argv){
vector<size_t> samples(std::stol(argv[2]));
auto start = std::clock();
std::iota(samples.begin(),samples.end(),0);
sample(std::stol(argv[1]),samples.begin(),samples.end());
cout << std::setw(12) << (std::clock()-start);
}
一些注意事项:std::shuffle 总是随机播放整个范围,但是当您只需要 k 个项目时,您可以在第 k 个元素处使用 fisher-yates 随机播放停止,使其成为最快的方法从已经存在的样本中抽样。
所以这是我想出的解决方案,它将以随机顺序生成样本,而不是以稍后需要打乱顺序的确定性方式:
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;
}
正如 Yksisarvinen 中指出的那样
的回答,C++17在<algorithm>
中提供了std::sample
应该有用。不幸的是,它对迭代器的使用使得直接使用整数变得尴尬,即没有构建一个大的临时 array/vector,我让它有效工作的唯一方法是使用大量样板代码:
#include <algorithm>
#include <iostream>
#include <iterator>
#include <random>
template<typename I>
class boxed_iterator {
I i;
public:
typedef I difference_type;
typedef I value_type;
typedef I pointer;
typedef I reference;
typedef std::random_access_iterator_tag iterator_category;
boxed_iterator(I i) : i{i} {}
bool operator==(boxed_iterator<I> &other) { return i == other.i; }
I operator-(boxed_iterator<I> &other) { return i - other.i; }
I operator++() { return i++; }
I operator*() { return i; }
};
给我们一些与 std::sample
:
一起使用时不会太痛苦的东西
int main()
{
std::vector<int> result;
auto rng = std::mt19937{std::random_device{}()};
// sample five values without replacement from [1, 100]
std::sample(
boxed_iterator{1}, boxed_iterator{101},
std::back_inserter(result), 5, rng);
for (auto i : result) {
std::cout << i << ' ';
}
}
如果不需要 boxed_iterator
就好了,如果有人能告诉我怎么做就好了!
我正在努力将 MATLAB 模拟移植到 C++ 中。为此,我试图复制 MATLAB 的 randsample() function。我还没有想出一个有效的方法来做到这一点。
所以我问大家,在 C++ 中,如何最好地从范围 0:n-1(对于 n > k)中随机抽取 k 个数字而不进行替换?
我考虑过以下伪代码(灵感来自 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]
这里的缺点也是需要先构建一个庞大的数组。这似乎 slow/clunky 矫枉过正。
如果你能帮忙的话,我很乐意在这里提供一些指导。我对理论不太感兴趣(算法很有趣,但现在与我的需求无关),而不是用 C++ 实现它的最佳方法。
提前致谢!
Bob Floyd 创建了一个使用集合的随机样本算法。中间结构大小与您要获取的样本大小成正比。
它的工作原理是随机生成 K 个数字并将它们添加到一个集合中。如果生成的数字碰巧已经存在于集合中,它会放置一个计数器的值,而不是保证还没有看到。从而保证在线性时间内运行,不需要很大的中间结构。它仍然具有很好的随机分布特性。
此代码基本上是从 Programming Pearls 中提取的,并进行了一些修改以使用更现代的 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;
}
此代码尚未经过测试。
这是一种不需要生成和洗牌巨大列表的方法,以防 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;
}
从 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
Bob Floyds 采样是一个很好的解决方案。 Reservoir sampling 然而,当 k 与 N 处于同一数量级时,这可能是一个不错的选择。
水库采样:
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;
}
随机抽样不完整:
#include <vector>
#include <random>
#include <algorithm>
#include <chrono>
#include <iostream>
#include <iomanip>
using std::vector;
using std::uniform_int_distribution;
using std::shuffle;
using std::cout;
using std::swap;
template<class iterator,class generator>
void inline shuffle(iterator beg,iterator unt,iterator end,generator gen){
const size_t n = end-beg;
for (;beg!=unt;++beg) {
size_t i=end-beg;
size_t r=uniform_int_distribution<size_t>(0,i)(gen);
swap(*beg,*(beg+r));
}
}
template<class iterator>
vector<size_t> sample(const size_t& k,iterator beg,iterator end) {
vector<size_t> sample(k);
std::default_random_engine gen;
if (k<(end-beg)/2) {
shuffle(beg,beg+k,end,gen);
for (size_t i=0;i!=k;(++i,++beg)) sample[i] = *beg;
} else {
const size_t l = end-beg-k;
shuffle(beg,beg+l,end,gen);
beg+=l;
for (size_t i=0;i!=k;(++i,++beg)) sample[i] = *beg;
}
return sample;
}
int main(int argc,char** argv){
vector<size_t> samples(std::stol(argv[2]));
auto start = std::clock();
std::iota(samples.begin(),samples.end(),0);
sample(std::stol(argv[1]),samples.begin(),samples.end());
cout << std::setw(12) << (std::clock()-start);
}
一些注意事项:std::shuffle 总是随机播放整个范围,但是当您只需要 k 个项目时,您可以在第 k 个元素处使用 fisher-yates 随机播放停止,使其成为最快的方法从已经存在的样本中抽样。
所以这是我想出的解决方案,它将以随机顺序生成样本,而不是以稍后需要打乱顺序的确定性方式:
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;
}
正如 Yksisarvinen 中指出的那样
的回答,C++17在<algorithm>
中提供了std::sample
应该有用。不幸的是,它对迭代器的使用使得直接使用整数变得尴尬,即没有构建一个大的临时 array/vector,我让它有效工作的唯一方法是使用大量样板代码:
#include <algorithm>
#include <iostream>
#include <iterator>
#include <random>
template<typename I>
class boxed_iterator {
I i;
public:
typedef I difference_type;
typedef I value_type;
typedef I pointer;
typedef I reference;
typedef std::random_access_iterator_tag iterator_category;
boxed_iterator(I i) : i{i} {}
bool operator==(boxed_iterator<I> &other) { return i == other.i; }
I operator-(boxed_iterator<I> &other) { return i - other.i; }
I operator++() { return i++; }
I operator*() { return i; }
};
给我们一些与 std::sample
:
int main()
{
std::vector<int> result;
auto rng = std::mt19937{std::random_device{}()};
// sample five values without replacement from [1, 100]
std::sample(
boxed_iterator{1}, boxed_iterator{101},
std::back_inserter(result), 5, rng);
for (auto i : result) {
std::cout << i << ' ';
}
}
如果不需要 boxed_iterator
就好了,如果有人能告诉我怎么做就好了!