【问题标题】:Simulate random iteration of array模拟数组的随机迭代
【发布时间】:2015-05-05 09:34:57
【问题描述】:

我有一个给定大小的数组。我想以伪随机顺序遍历它,保持数组完整并访问每个元素一次。最好将当前状态存储在几个整数中。

我知道you can't have full randomness without storing full array,但我不需要真正随机的顺序。我需要它被用户认为是随机的。解决方案应使用亚线性空间。

一个可能的建议 - 使用大质数 - 是given here。这个解决方案的问题是有一个明显的固定步骤(采取模块数组大小)。我更喜欢一个不是那么明显非随机的解决方案。有没有更好的解决方案?

【问题讨论】:

  • 您是否对多次访问给定元素感到满意?即您是在寻找元素的随机排列还是元素的随机序列?您引用的问题与第一个问题有关(也称为改组)。
  • 许多(大多数?)伪随机数生成器可以由算法和一个或两个整数来定义,以描述它们的当前状态。考虑到这些因素,他们未来的行为完全是确定性的,尽管显然是随机的——这就是 pseudo 在这里的意思。你在找什么不是 PRNG 的?
  • 显然,我需要避免重复(否则我只能跳到随机元素)。我编辑了问题,添加了该信息。实际上,所需的解决方案应该以允许从任何状态以 N 步遍历 N 个元素的方式存储当前状态。
  • 显然,我需要避免重复 不,在您编辑之前一点都不明显,它对潜在的解决方案有重大影响。
  • TBH,我认为原件已经很清楚了,I want to traverse it is pseudorandom order。 Traverse 确实意味着每个元素都被访问过一次,但可能只有我和我的英语不好。

标签: c++ arrays algorithm random


【解决方案1】:

这是一个java解决方案,可以很容易地转换成C++,类似于上面M Oehm的解决方案,只是选择LCG参数的方式不同。

import java.util.Enumeration;
import java.util.Random;

public class RandomPermuteIterator implements Enumeration<Long> {
    int c = 1013904223, a = 1664525;
    long seed, N, m, next;
    boolean hasNext = true;

    public RandomPermuteIterator(long N) throws Exception {
        if (N <= 0 || N > Math.pow(2, 62)) throw new Exception("Unsupported size: " + N);
        this.N = N;
        m = (long) Math.pow(2, Math.ceil(Math.log(N) / Math.log(2)));
        next = seed = new Random().nextInt((int) Math.min(N, Integer.MAX_VALUE));
    }

    public static void main(String[] args) throws Exception {
        RandomPermuteIterator r = new RandomPermuteIterator(100);
        while (r.hasMoreElements()) System.out.print(r.nextElement() + " ");
        //output:50 52 3 6 45 40 26 49 92 11 80 2 4 19 86 61 65 44 27 62 5 32 82 9 84 35 38 77 72 7 ...
    }

    @Override
    public boolean hasMoreElements() {
        return hasNext;
    }

    @Override
    public Long nextElement() {
        next = (a * next + c) % m;
        while (next >= N) next = (a * next + c) % m;
        if (next == seed) hasNext = false;
        return  next;
    }
}

【讨论】:

    【解决方案2】:

    如果你能得到一个你可以控制最大周期的随机生成器,那么模拟洗牌的随机生成器的想法是很好的。

    Linear Congruential Generator 使用以下公式计算一个随机数:

    x[i + 1] = (a * x[i] + c) % m;
    

    最大周期为m,当满足以下属性时达到:

    • 参数cm 是互质的。
    • 对于每个素数 r 除以 ma - 1 是 r 的倍数。
    • 如果 m 是 4 的倍数,那么 a - 1 也是 4 的倍数。

    我的第一个 darft 涉及使 m 成为数组长度之后 4 的下一个倍数,然后找到合适的 ac 值。这是 (a) 大量工作,并且 (b) 有时会产生非常明显的结果。

    我重新考虑了这种方法。我们可以使 m 成为数组长度适合的 2 的最小幂。m 的唯一素数因子是 2,这将使每个奇数相对于它。除了 1 和 2,m 将被 4 整除,这意味着我们必须使 a - 1 成为 4 的倍数。

    m 大于数组长度意味着我们必须丢弃所有非法数组索引的值。这最多每隔一个回合就会发生一次,应该可以忽略不计。

    下面的代码产生具有精确周期m 的伪随机数。我已经避免了 ac 的琐碎值,并且在我(不是太多)现场检查中,结果看起来还不错。至少没有明显的循环模式。

    所以:

    class RandomIndexer
    {
    public:
        RandomIndexer(size_t length) : len(length)
        {
            m = 8;
            while (m < length) m <<= 1;
    
            c = m / 6 + uniform(5 * m / 6);
            c |= 1;
    
            a = m / 12 * uniform(m / 6);
            a = 4*a + 1;
            x = uniform(m);                      
        }
    
        size_t next()
        {
            do { x = (a*x + c) % m; } while (x >= len);
    
            return x;
        }
    
    private:
        static size_t uniform(size_t m)
        {
            double p = std::rand() / (1.0 + RAND_MAX);
    
            return static_cast<int>(m * p);
        }
    
        size_t len;
        size_t x;
        size_t a;
        size_t c;
        size_t m;
    };
    

    然后您可以像这样使用生成器:

    std::vector<int> list;
    for (size_t i = 0; i < 3; i++) list.push_back(i);
    
    RandomIndexer ix(list.size());
    for (size_t i = 0; i < list.size(); i++) {
        std::cout << list[ix.next()]<< std::endl;
    }
    

    我知道这仍然不是一个很好的随机数生成器,但它相当快,不需要数组的副本并且似乎可以正常工作。

    如果随机选择 ac 的方法产生不好的结果,最好将生成器限制为 2 的某些幂并进行硬编码已证明良好的文献价值。

    【讨论】:

    • 您确定while (p*p &lt; m) 条件是否足够?如果有一个更大的素数除以 m 怎么办?例如,956 = 4 * 239 而你的算法(如果我检查正确的话)得出 a=5,这是错误的。
    • 这个解决方案的另一个问题是它“看起来”不是随机的——很容易看出两个后续项目之间有一个固定的步长。
    • 你是对的。我们必须找到所有因素。在 m 是 4 和素数的乘积的情况下,a 实际上是 1。(它是 m + 1,但模算术意味着它和 1 一样好。)我已经更正了代码。
    • LCG 不是一个好的发电机。 a = 1 的情况使它变得非常糟糕。在这种情况下,a 可能应该与另一个(随机?)数字相乘以使其不那么明显。找到好的参数集需要一些研究。 (当然,如果数组只是一个枚举,那么这种模式会更加明显。由于练习的目的是索引数组,因此数组的结构可能会隐藏规律。)
    • 好的,改变计划。使 m 尽可能接近数组长度的整个过程是无稽之谈。我们可以跳过整个寻找素数的工作,只使用二的下一个幂。我已经更新了代码并运行了一些检查,结果对我来说看起来还不错。 (可能还有很大的改进空间,但我认为这是一个可行的解决方案。)
    【解决方案3】:

    这个算法怎么样?

    以伪伪随机遍历大小为n的数组。

    1. 创建一个大小为 k 的小数组
    2. 用大素数法填充小数组,i = 0
    3. 使用 RNG 从小数组 i += 1 中随机删除一个位置
    4. 如果 i
    5. 如果我

    k 越高,获得的随机性就越大。这种方法将允许您延迟从素数方法生成数字。

    通过创建另一个数组“skip-list”,可以使用类似的方法在序列中生成比预期更早的数字。随机选择序列后面的项目,使用它们遍历下一个位置,然后将它们添加到跳过列表中。当它们自然到达时,它们会在跳过列表中搜索并被抑制,然后从跳过列表中删除,此时您可以将另一个项目随机添加到跳过列表中。

    【讨论】:

    • 我最终得到了跳过列表主题的变体。所有项目都是通过大素数方法生成的,但每次我需要选择一个数字时,我都会先将随机数的项目添加到跳过列表中(最多为一个小的固定大小)。然后,我要么从跳过列表中随机选择,要么返回通过大素数方法计算的下一个数字(这也是随机决定的)。它很容易实现,而且数字看起来很随机。
    【解决方案4】:

    您提到的二次余数(“使用大素数”)是众所周知的,可以工作,并保证每个元素都只迭代一次(如果需要,但似乎严格来说并非如此?) .不幸的是,它们并不是“看起来很随意”,除了要使其正常工作之外,还有一些其他的模数要求。
    Jeff Preshing 的网站上有一个页面详细描述了该技术并建议feed the output of the residue generator into the generator again with a fixed offset

    但是,由于您说您只需要“用户认为是随机的”,因此您似乎可以用连续整数提供散列函数(例如,cityhash 或 siphash)。输出将是一个“随机”整数,至少到目前为止会有一个严格的 1:1 映射(因为可能的哈希值比输入多得多)。

    现在的问题是您的数组很可能没有那么大,因此您需要以某种方式缩小这些生成索引的范围而不生成重复项(这很难)。

    显而易见的解决方案(取模)不会起作用,因为它几乎可以保证您得到很多重复。

    使用位掩码将范围限制为 2 的下一个更大的幂应该可以在不引入偏差的情况下工作,并且丢弃超出范围的索引(生成新索引)也应该可以工作。请注意,这需要不确定的时间 - 但是这两者的组合平均来说应该可以很好地工作(最多尝试几次)。

    否则,“真正有效”的唯一解决方案是改组一系列索引,正如 Kamil Kilolajczyk 所指出的那样(尽管您不希望这样)。

    【讨论】:

      【解决方案5】:

      正如其他人所指出的,您可以通过改组数组索引数组然后遵循它来预先创建一种“飞行计划”。这违反了“最好将当前状态存储在几个整数中”的约束,但这真的重要吗? 是否存在严格的性能限制?毕竟,我相信如果您不接受重复,那么您需要将您已经访问过的项目存储在某个地方或以某种方式。

      或者,您可以选择 intrusive 解决方案并在数组的每个元素内存储一个 bool,告诉您该元素是否已被选中。这可以通过使用继承(根据需要多个)以几乎干净的方式完成。
      这个解决方案带来了许多问题,例如线程安全,当然它违反了“保持数组完整”的约束。

      【讨论】:

      • 再一次,OP 要求不要存储类似大小的数组,或者换句话说 - 他要求在 o(n) 空间 [small o here] 中找到解决方案。
      • 我明白这一点,实际上我已经非常清楚地指出了这一点。我提出了部分的替代解决方案,可以放松一些限制,并且可能是一个很好的权衡。例如,与洗牌索引数组相比,侵入式解决方案可能会为每个元素节省几个字节。问题是:你读过答案吗? ;)
      • 也许这可以与使用可以强制种子的随机数生成器结合使用。该程序将选择一个随机种子。每次需要打乱后的数据时,都可以从原始数组和种子中复制出来。
      • @gd1 我假设随机数生成器将用于驱动Fisher-Yates shuffle 的明显选择。
      • 我明白你的意思,但实际上很难就你的“不真实”随机性达成共识,因为这是一个很难形式化的模糊概念。即使我们提出了一个“有点”随机的解决方案,你可能会说它不够随机,或者在你想要的伪意义上不是随机的,当我们指出真正的随机性是无法实现的时,你可能总是说你不需要它,因为你对你的“一些”随机性很好。我会考虑一下:)
      【解决方案6】:

      【讨论】:

      • OP 在不操作数组或存储新数组的情况下询问。
      • 但是使用它,您可以创建新的简单索引数组 0..length-1(不是对象数组的克隆),random_shuffle 它们并使用这些随机索引获取对象
      • 我的条件是避免类似大小的额外数据。显然,如果我们允许一个数组的副本,random_shuffle 就可以了。
      • 是的,但我建议创建比复杂对象更小的整数的临时数组......或者这个数组非常大,甚至不可接受的数以万计的整数临时数组,然后好吧,我错了:)
      猜你喜欢
      • 2023-03-10
      • 1970-01-01
      • 2013-09-30
      • 1970-01-01
      • 1970-01-01
      • 2012-03-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多