O(1)内存中的随机序列迭代?

时间:2012-09-17 13:41:55

标签: c++ stl iterator complexity-theory permutation

假设您想以随机顺序迭代序列[0到n],只访问每个元素一次。有没有办法在 O (1)内存中执行此操作,即不使用std::iota创建[1..n]序列并通过std::random_shuffle运行它?

某种迭代器以随机顺序吐出序列将是最佳的。

要求是应该可以通过选择另一个种子来获得另一个随机顺序。

6 个答案:

答案 0 :(得分:8)

如果你可以就地改变序列,你可以简单地从0-N重复绘制一个随机数,然后删除你访问过的元素,或者将它交换到最后,或者这样的方案。

答案 1 :(得分:6)

理论上,如果你构建了一个随机数生成器,其周期正好是 n ,并且覆盖了0..n中的所有值,那么运行一次这样可以得到你喜欢的东西。

当然,这可能不是一般解决方案,至少如果你正在寻找动态的东西,因为你必须预先创建PRNG,你如何做到这一点取决于n。

答案 2 :(得分:1)

嗯......想一想。你怎么知道之前访问过哪些元素?

简短的回答:你做不到。 (编辑好吧,除非你计算无状态的伪随机生成器,但正如你在命令中说明的那样,这对于一般情况似乎不可行)

然而,根据实际顺序,'标记'元素可能是可行的访问 _in-place _ 因此技术上需要O(n)存储,但是算法的没有额外的存储

示例:

const int VISITED_BIT = 0x8000; // arbitrary example

bool extract(int i) { return (i & ~VISITED_BIT); }    
bool visited(int i) { return (i & VISITED_BIT); }    
bool markvisited(int& i) { i |= VISITED_BIT); }

int main()
{
    std::vector<int> v = {2,3,4,5,6};

    int remain = v.size();
    while (remain>0)
    {
        size_t idx = rand(); // or something
        if (visited(v[idx]))
            continue;

        std::cout << "processing item #" << idx << ": " << extract(v[idx]) << "\n";
        markvisited(v[idx]);
        remain--;
    }
}

答案 3 :(得分:1)

与大多数算法问题一样,存在时空权衡;如果您乐意使用O(n ^ 2)时间生成所有排列,则可以在O(1)空间中解决此问题。除了几个临时变量之外,唯一需要的存储是随机数种子本身(或者,在本例中是PRNG对象),因为这足以重新生成伪随机数序列。

请注意,您必须在每次通话时为此功能提供相同的PRNG,并且您不能将其用于任何其他目的。

#include <random>

template<typename PRNG, typename INT>
INT random_permutation_element(INT k, INT n, PRNG prng) {
  typedef std::uniform_int_distribution<INT> dis;
  INT i = 0;
  for (; i < k; ++i) dis(0, i)(prng);
  INT result = dis(0, i)(prng);
  for (++i; i < n; ++i) if (dis(0, i)(prng) <= result) ++result;
  return result;
}

这是一个快速而肮脏的安全带。 ./test 1000 3生成1000个完整的长度为3的排列; ./test 10 1000000 0 5生成长度为100万的10种排列中的每一种的前五个元素。

#include <iostream>

int main(int argc, char** argv) {
  std::random_device rd;
  std::mt19937 seed_gen(rd());
  int count = std::stoi(argv[1]);
  int size = std::stoi(argv[2]);
  int seglow = 0;
  int seglim = size;
  if (argc > 3) seglow = std::stoi(argv[3]);
  if (argc > 4) seglim = std::stoi(argv[4]);
  while (count-- > 0) {
    std::mt19937 prng(seed_gen());
    for (int i = seglow; i < seglim; ++i)
      std::cout << random_permutation_element(i, size, prng)
                << (i < seglim - 1 ? ' ' : '\n');
  }
  return 0;
}

如果你不太可能完成任何给定的排列,有一种更快的方法可以做到这一点,但这种写作方式看起来更好,并且可能更容易理解。 (另一种方法是以相反的顺序生成数字,这意味着你可以在生成k之后停止,但你必须做两次,首先得到结果然后调整它。)

答案 4 :(得分:0)

不,没有,想一想,程序必须记住它访问过的地方。如果有一个可以随机访问它们的迭代器,迭代器内部必须以某种方式跟踪它,你仍然会使用内存。

答案 5 :(得分:0)

我刚为这类事物构建了一个结构 - 我生成了一个Heap结构(min或max,无关紧要)。但是为了比较,我使用随机数而不是使用键值。因此,插入堆中的项目以随机顺序放置。然后你可以返回形成堆的基本结构的数组(将随机排序),或者你可以逐个弹出元素,然后以随机顺序返回它们。如果将这种类型的容器用作主存储(而不是将数组与堆分开),则不存在额外的内存复杂性,因为无论如何它只是一个数组。时间复杂度是插入的O(log N),弹出顶部元素的O(log N)。改组就像弹出和重新插入每个元素一样简单,O(N log N)。

我甚至构建了一个花哨的枚举器(它是C#,但你可以用C ++迭代器做同样的事情),在你迭代到最后之后自动改组。这意味着每次你可以多次遍历列表(没有弹出)并且每次都获得不同的顺序,代价是每次完整迭代后的O(N log N)shuffle。 (想想就像一副纸牌。在每张卡片进入丢弃堆后,你重新洗牌,以便下次不能以相同的顺序进行。)