我想要一个函数,它将从一组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和可容忍的内存占用之间的折衷。
答案 0 :(得分:2)
首先,打折任何使用O(n)内存然后讨论引用底层数组的解决方案似乎是不合理的。你有一个阵列。洗牌吧。如果这不起作用或不够快,请回答我们的问题。
您只需要执行一次完整的随机播放。之后,从索引n
中绘制,将该元素与位于其前面的元素交换并增加n
,模数元素计数。例如,对于这么大的数据集,我会使用something like this。
素数是散列的选项,但可能与您的想法不同。使用两个Mersenne素数(low
和high
,可能0xefff
和0xefffffff
),您应该能够提出更通用的哈希算法。
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;
}
将seed
和key
初始化为随机值,您就可以了。
使用反函数让我确定seed
必须强制使用的内容
rand_no_rep()
生成给定的输出;使测试更容易。
到目前为止,我已经检查过常量a
的情况,然后是常数
b
。对于ROUNDS==1
对,恰好50%的密钥碰撞(每个密钥
一对碰撞是由一对a
和b
组成的;他们不会在0,1或其他任何方面收敛。也就是说
各种k
,针对多个a
的特定b
- 后续 - k
个案发生
(此必须至少发生一次)。后续值值不会发生冲突
那种情况,所以不同的键不会在不同的时间落入同一个循环
位置。每个k
都会给出一个独特的循环。
50%的碰撞来自25%,当它们被添加到列表中时并不是唯一的(计算本身,计算它遇到的那个人)。这可能听起来很糟糕,但它实际上低于生日悖论逻辑所暗示的。随机选择,未成为唯一的新条目的百分比看起来会在36%和37%之间收敛。存在&#34;比随机更好&#34;就随机性而言,显然更糟而不是随机,但这就是他们称为伪随机数的原因。
将其扩展到ROUNDS==2
,我们希望确保第二轮不会
取消或简单地重复第一次的效果。
这很重要,因为这意味着多轮是浪费
时间和记忆,并且该功能不能用于任何参数
实质性的。如果mix()
包含全部线性,则可能会发生这种情况
操作(比如,乘法和加法,mod RANGE
)。在那种情况下所有的
参数可以相乘/相加以产生单个参数
一轮会有同样的效果。那会令人失望,
因为它会将可达到的排列数量减少到正好的大小
那个参数,如果集合那么小,那么就会有更多的工作
需要确保它是一个好的,有代表性的集合。
因此,我们希望从两轮中看到的是一大堆可能的结果
永远不会通过一轮实现。证明这一点的一种方法是寻找
原始b
- 跟随 - a
个案例,其中包含我们想要的其他参数c
查看c
和a
之后的所有可能b
。
我们从一轮测试中知道,在50%的情况下,只有一个c
可以跟随a
和b
,因为只有一个k
可以放置b
在a
之后立即。我们也知道,a
和b
对中有25%是。k
和c
无法达到(是进入的一半对后留下的差距
碰撞而不是新的唯一值),最后25%出现在两个
不同的a
。
我得到的结果是,如果可以自由选择两个键,那么它是可能的
在给定的b
和a
之后查找b
的值的五个八分之一。
大约四分之一的a
/ b
对无法访问(这是不太可预测的,
现在,因为潜在的中间映射进出
重复或无法访问的案例)并且四分之一会出现c
,{{1}}和{{1}}
一起分成两个序列(后来分开)。
我认为从一轮和另一轮的差异可以推断出很多 二,但我可能错了,我需要仔细检查。进一步 测试越来越难;或者至少慢一点,除非我更仔细地考虑如何 我要去做。
我还没有证明,在它可以产生的一系列排列中,它们都是同样可能的;但这通常也不能保证任何其他PRNG。
PRNG的速度相当慢,但它很容易适合SIMD。