【问题标题】:Random sequence iteration in O(1) memory?O(1)内存中的随机序列迭代?
【发布时间】:2012-09-17 13:41:55
【问题描述】:

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

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

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

【问题讨论】:

  • n的值是否有上限?
  • 理论上,在 [0, n] 范围内的均匀分布的随机数可以完成这项工作。但是,我不确定如何使用 C++11 随机工具为此任务编写 正确 代码。
  • @AraK 均匀分布的 PRNG 是否会只发射每个元素一次?
  • 我认为你不能按照你的要求去做。但是你可以通过创建一个指向你元素的向量来最小化内存占用并在上面使用std::random_shuffle
  • 请注意,即使您不能在 O(1) 内存中执行此操作,如果您可以接受并忽略重复,您也可以降至 n 位(可能需要更长的时间,并且可能如果您的 PRNG 足够有限,甚至会永远循环)。这将带您走很长一段路,您只需要 32 分之一的空间来显式存储每个数字(假设为 32 位整数)。

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


【解决方案1】:

如果您可以就地改变序列,您可以简单地重复从 0-N 中抽取一个随机数,然后擦除您访问的元素,或将其交换到末尾,或类似方案。

【讨论】:

  • 这可能是最实用的解决方案。它将内存占用量从 2n 减少到 n。我想将序列存储在双端队列中以允许释放不再需要的堆块是有意义的。
  • +1 这就是我过去的做法。给定一个合理的输出范围大小,使用任何种子 RNG % #items-remaining, remote 该项目作为输出值并重复。
【解决方案2】:

理论上,如果你构建了一个随机数生成器,它的周期正好是 n,并且涵盖了 0..n 中的所有值,那么运行一次就会得到你喜欢的结果。

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

【讨论】:

  • 我对伪随机数生成器的了解非常有限。是否可以为任意时间段 n 构建/参数化随机数生成器?是否可以生成多个具有相同周期的不同生成器?
  • 另外,PRNG 本身需要有O(1) 内部状态——有一些非常明显的方法可以构建这样一个违反该状态的 PRNG ;-)
  • 您可以为许多 n 值创建一个全周期线性同余 RNG,并且这些值已知具有 O(1) 内部状态。在给定 n 值的情况下,我不知道有任何通用方法来计算此类生成器。我所知道的是,对于 n 的某些值,可以构建 O(1) 内存 PRNG ,并且对于 n 的某些值,可以生成多个序列。我不知道如何为任意 n 动态构建这些(或者即使存在通用算法来执行此操作——在 LCRNG 的情况下,对于给你一个完整周期的内容存在 IFF 限制)。
  • 感谢这些有见地的 cmets。我得出结论,“周期为 n 的 PRNG”解决方案在一般情况下是不可行的。特别是如果还希望能够生成任意数量的周期为 n 的不同 PRNG 以获得不同的排列。
  • 如果 n 可以是 2x 或 2x - 1,则基于原始多项式的 LFSR 将完全按照您的描述进行。
【解决方案3】:

嗯...想一想。您如何“知道”之前访问过哪些元素?

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

然而,根据实际顺序,将元素“标记”为已访问 _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--;
    }
}

【讨论】:

  • 我不相信。历史/状态可以编码在当前随机索引中。毕竟,这就是 LCG 的工作方式(请参阅 Ray 的回答)。这很困难,但并非从根本上不可行,并且违背了您的主张。
  • 这不是不可能,只是真的很难。你必须有一个算法从一个范围内只生成一次随机数。这个算法可能对任何 n 都存在(虽然我没有检查),问题只是它不会对 n 和 n+1 相同。
  • 两位 cmets 似乎都在讨论我选择不描述的解决方案,这真是太有趣了。呵呵
  • @sehe 我评论了之前你添加了关于使用 PRNG 状态的警告,然后你添加了下一个最佳解决方案的详细描述。跨度>
  • @KonradRudolph 你说得对。我的代码示例在您发表评论后的 50 年代。这解释了:)
【解决方案4】:

与大多数算法问题一样,存在时空权衡;如果您乐于使用 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 生成 10 个长度为 100 万的排列的前五个元素。

#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 个后停止,但您必须执行两次,首先获得结果,然后再进行调整。)

【讨论】:

  • -1 用于当您需要在调用之间保留不变量时不将其包装在类中,以及用于全大写模板参数。
  • @DeadMG:好的。很高兴您正在阅读我两年前的答案。有一个很好的论据可以将其包装在一个类中,而不是在每次调用时都提供 state 参数,但是无状态函数式风格也有它的追随者,我的历史更倾向于函数式方向。如果您想将模板参数编辑为您喜欢的样式,请在我的祝福下这样做。
【解决方案5】:

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

【讨论】:

  • 或者,您也可以简单地删除您去过的地方。
  • 这是粗心大意的无效立场。有很多明显的 O(n) 空间迭代方案遵循从不访问相同元素两次的不变量。我能想到的都不是伪随机的事实并不能证明不存在。
【解决方案6】:

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

我什至构建了一个奇特的 Enumerator(它是 C#,但您可以使用 C++ Iterator 执行相同的操作),它会在您迭代到最后时自动随机播放。这意味着每次您可以多次迭代列表(不弹出)并每次获得不同的顺序,但每次完整迭代后都会以 O(N log N) 洗牌为代价。 (像一副纸牌一样思考。每张纸牌都进入弃牌堆后,您重新洗牌,以免下次以相同的顺序获得它们。)

【讨论】:

    猜你喜欢
    • 2015-07-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-04
    • 2013-12-10
    相关资源
    最近更新 更多