用于选择随机子集的通用算法实现

时间:2016-01-30 11:45:13

标签: c++ algorithm random c++14 generic-programming

假设我们要从总大小m中选择一个大小为n的随机子集。由于可以使用S = {0, 1, 2, ..., (n - 1)}中的唯一索引来标识总集中的每个元素。问题相当于从m中随机选择S个不同的元素。

一个简单的算法将重复调用伪随机数生成器rand以从S生成随机数。如果之前已生成过数字,请再试一次。算法终止,直到生成m个不同的数字。此算法的最佳空间复杂度为O(1),但可能会rand调用m次以上。

我更关心的是时间复杂度而不是空间复杂性,如果合理的话,我会乐意交换空间。所以我实现了以下算法。它会rand完全调用min{m, (n - m)}次,但价格会增加O(n)的空间复杂度。 (原始代码可以找到here

template <typename Clock = std::chrono::high_resolution_clock>
auto tick_count() {
  return Clock::now().time_since_epoch().count();
}

template <typename OutIt, typename RAND = std::minstd_rand,
          typename Uint = typename RAND::result_type>
void random_subset(std::size_t m, std::size_t n, OutIt it, RAND&& rand =
                   RAND(static_cast<Uint>(tick_count()))) {
  assert(n - 1 <= rand.max());
  assert(m <= n);
  if (m == 0) return;
  auto swapped = false;
  auto tmp = n - m;
  if (tmp < m) {
    m = tmp;
    swapped = true;
  }
  std::vector<std::size_t> indices(n);
  std::iota(indices.begin(), indices.end(), static_cast<std::size_t>(0));
  auto back_it = indices.end();
  for (std::size_t i = 0; i < m; ++i) {
    auto idx = rand() % (n - i);
    std::swap(indices[idx], *--back_it);
  }
  swapped ? std::copy(indices.begin(), back_it, it) :
            std::copy(back_it, indices.end(), it);
}

我想知道算法是否可以在性能方面进一步改进。对通用实现的改进也很受欢迎。

1 个答案:

答案 0 :(得分:2)

也许您可以使用Fisher-Yates algorithm的非常小的变体进行随机改组,特别是second variant of the Durstendfeld version

-- To shuffle an array a of n elements (indices 0..n-1):
for i from 0 to n−2 do
     j ← random integer such that 0 ≤ j < n-i
     exchange a[i] and a[i+j]

只需将循环终止从 n - 2 更改为您需要的。

在证明中,循环不变量是指一旦传递了索引 i ,直到它的数组就是随机混乱。因此,您可以提前终止所需的结果。