从非常大的范围返回非重复的随机值

时间:2016-07-20 04:46:08

标签: c++ c random permutation

我想要一个函数,它将从一组n个整数,0到n-1产生k个伪随机值,而不重复任何先前的结果。 k小于或等于n。 O(n)内存是不可接受的,因为n的大小和我需要重新洗牌的频率。

这些是我到目前为止所考虑的方法:

阵列: 通常,如果我想要无重复的随机值,我会对数组进行混洗,但那是O(n)内存。 n可能太大而无法工作。

long nextvalue(void) {
  static long array[4000000000];
  static int s = 0;
  if (s == 0) {
    for (int i = 0; i < 4000000000; i++) array[i] = i;
    shuffle(array, 4000000000);
  }
  return array[s++];
}

n州PRNG : 可以设计各种随机数生成器,以便在此期间具有n的句点并访问n个唯一状态。最简单的例子是:

long nextvalue(void) {
static long s = 0;
static const long i = 1009; // assumed co-prime to n
  s = (s + i) % n;
  return s;
}

问题在于,对于给定的n,动态设计一个好的PRNG并不一定容易,并且如果PRNG没有大量的PRNG,它就不太可能接近公平的洗牌。可变参数(甚至更难设计)。但也许有一个我不知道的好事。

m位哈希: 如果集合的大小是2的幂,则可以设计完美的散列函数f(),该函数执行从范围中的任何值到范围中的某个其他值的1:1映射,其中每个输入产生独特的输出。使用此函数,我可以简单地维护一个静态计数器s,并将生成器实现为:

long nextvalue(void) {
  static long s = 0;
  return f(s++);
}

这并不理想,因为结果的顺序由f()确定,而不是随机值,因此它会遇到与上述相同的所有问题。

NPOT哈希: 原则上,我可以使用与上面相同的设计原则来定义f()的版本,该版本在任意基础上工作,甚至是复合,与所需的范围兼容;但这可能很难,我可能会弄错。相反,可以为下一个大于或等于n的2的幂定义一个函数,并在此构造中使用:

long nextvalue(void) {
  static long s = 0;
  long x = s++;
  do { x = f(x); } while (x >= n);
}

但是这个仍然有同样的问题(不太可能给出一个很好的近似的公平洗牌)。

有没有更好的方法来处理这种情况?或者我可能只需要f()的一个很好的函数,它具有高度可参数且易于设计以准确访问n个离散状态。

我正在考虑的一件事是类似哈希的操作,我设法通过精心设计的映射使第一个j结果完全随机,然后j和{{1}之间的任何结果只会在该模式上进行推断(尽管是以可预测的方式)。然后可以选择值k以找到公平的shuffle和可容忍的内存占用之间的折衷。

2 个答案:

答案 0 :(得分:2)

首先,打折任何使用O(n)内存然后讨论引用底层数组的解决方案似乎是不合理的。你有一个阵列。洗牌吧。如果这不起作用或不够快,请回答我们的问题。

您只需要执行一次完整的随机播放。之后,从索引n中绘制,将该元素与位于其前面的元素交换并增加n,模数元素计数。例如,对于这么大的数据集,我会使用something like this

素数是散列的选项,但可能与您的想法不同。使用两个Mersenne素数(lowhigh,可能0xefff0xefffffff),您应该能够提出更通用的哈希算法。

size_t hash(unsigned char *value, size_t value_size, size_t low, size_t high) {
    size_t x = 0;
    while (value_size--) {
        x += *value++;
        x *= low;
    }
    return x % high;
}
#define hash(value, value_size, low, high) (hash((void *) value, value_size, low, high))

这应该为大于大约两个八位字节的所有输入产生相当好的分布,例如零字节前缀的小麻烦异常。您可能希望以不同方式对待它们。

答案 1 :(得分:0)

所以......我最终做的就是深入挖掘已有的方法 试图证实他们接近公平洗牌的能力。

我带一个简单的计数器,它本身可以保证访问 每个范围内的值只有一次,然后加密&#39;它带有一个n位块 暗号。相反,我将范围扩大到2的幂,并应用1:1 功能;然后,如果结果超出范围,我重复排列直到结果 结果在范围内。

这可以保证最终完成,因为只有一个有限的 在2的幂范围内的超出范围值的数量,并且它们不能 进入一个永远超出范围的周期,因为这意味着某些东西 在循环中映射了两个不同的先前状态(一个来自 范围内集合,另一个来自超范围集合),这将使得 功能不是双射的。

所以我需要做的就是设计一个可参数化的函数,我可以调到它 任意位数。像这样:

uint64_t mix(uint64_t x, uint64_t k) {
  const int s0 = BITS * 4 / 5;
  const int s1 = BITS / 5 + (k & 1);
  const int s2 = BITS * 2 / 5;
  k |= 1;

  x *= k;
  x ^= (x & BITMASK) >> s0;
  x ^= (x << s1) & BITMASK;
  x ^= (x & BITMASK) >> s2;
  x += 0x9e3779b97f4a7c15;

  return x & BITMASK;
}

我知道它是双射的,因为我碰巧有它的反函数:

uint64_t unmix(uint64_t x, uint64_t k) {
  const int s0 = BITS * 4 / 5;
  const int s1 = BITS / 5 + (k & 1);
  const int s2 = BITS * 2 / 5;
  k |= 1;
  uint64_t kp = k * k;
  while ((kp & BITMASK) > 1) {
    k *= kp;
    kp *= kp;
  }

  x -= 0x9e3779b97f4a7c15;
  x ^= ((x & BITMASK) >> s2) ^ ((x & BITMASK) >> s2 * 2);
  x ^= (x << s1) ^ (x << s1 * 2) ^ (x << s1 * 3) ^ (x << s1 * 4) ^ (x << s1 * 5);
  x ^= (x & BITMASK) >> s0;
  x *= k;

  return x & BITMASK;
}

这允许我定义一个简单的可参数化的PRNG,如下所示:

uint64_t key[ROUNDS];
uint64_t seed = 0;
uint64_t rand_no_rep(void) {
  uint64_t x = seed++;
  do {
    for (int i = 0; i < ROUNDS; i++) x = mix(x, key[i]);
  } while (x >= RANGE);
  return x;
}

seedkey初始化为随机值,您就可以了。

使用反函数让我确定seed必须强制使用的内容 rand_no_rep()生成给定的输出;使测试更容易。

到目前为止,我已经检查过常量a的情况,然后是常数 b。对于ROUNDS==1对,恰好50%的密钥碰撞(每个密钥 一对碰撞是由一对ab组成的;他们不会在0,1或其他任何方面收敛。也就是说 各种k,针对多个a的特定b - 后续 - k个案发生 (此必须至少发生一次)。后续值值不会发生冲突 那种情况,所以不同的键不会在不同的时间落入同一个循环 位置。每个k都会给出一个独特的循环。

50%的碰撞来自25%,当它们被添加到列表中时并不是唯一的(计算本身,计算它遇到的那个人)。这可能听起来很糟糕,但它实际上低于生日悖论逻辑所暗示的。随机选择,未成为唯一的新条目的百分比看起来会在36%和37%之间收敛。存在&#34;比随机更好&#34;就随机性而言,显然更糟而不是随机,但这就是他们称为伪随机数的原因。

将其扩展到ROUNDS==2,我们希望确保第二轮不会 取消或简单地重复第一次的效果。

这很重要,因为这意味着多轮是浪费 时间和记忆,并且该功能不能用于任何参数 实质性的。如果mix()包含全部线性,则可能会发生这种情况 操作(比如,乘法和加法,mod RANGE)。在那种情况下所有的 参数可以相乘/相加以产生单个参数 一轮会有同样的效果。那会令人失望, 因为它会将可达到的排列数量减少到正好的大小 那个参数,如果集合那么小,那么就会有更多的工作 需要确保它是一个好的,有代表性的集合。

因此,我们希望从两轮中看到的是一大堆可能的结果 永远不会通过一轮实现。证明这一点的一种方法是寻找 原始b - 跟随 - a个案例,其中包含我们想要的其他参数c 查看ca之后的所有可能b

我们从一轮测试中知道,在50%的情况下,只有一个c 可以跟随ab,因为只有一个k可以放置ba之后立即。我们也知道,ab对中有25%是。kc 无法达到(是进入的一半对后留下的差距 碰撞而不是新的唯一值),最后25%出现在两个 不同的a

我得到的结果是,如果可以自由选择两个键,那么它是可能的 在给定的ba之后查找b的值的五个八分之一。 大约四分之一的a / b对无法访问(这是不太可预测的, 现在,因为潜在的中间映射进出 重复或无法访问的案例)并且四分之一会出现c,{{1}}和{{1}} 一起分成两个序列(后来分开)。

我认为从一轮和另一轮的差异可以推断出很多 二,但我可能错了,我需要仔细检查。进一步 测试越来越难;或者至少慢一点,除非我更仔细地考虑如何 我要去做。

我还没有证明,在它可以产生的一系列排列中,它们都是同样可能的;但这通常也不能保证任何其他PRNG。

PRNG的速度相当慢,但它很容易适合SIMD。