我希望列举固定空间中数字1..N的随机排列。这意味着我无法将所有数字存储在列表中。原因是N可能非常大,超过可用内存。我仍然希望能够一次一个地浏览这样的数字排列,只访问每个数字一次。
我知道这可以针对某些N来完成:许多随机数生成器随机循环遍历整个状态空间,但完全是。状态大小为32位的良好随机数发生器将发出数字0 ...(2 ^ 32)-1的排列。每个数字只需一次。
我想选择N作为任何数字,而不是限制为2的幂。有算法吗?
答案 0 :(得分:11)
最简单的方法可能是创建一个范围比你想要的更大范围的全范围PRNG,当它生成一个比你想要的更大的数字时,只需扔掉它就可以得到下一个。
另一种可能性相同的变化是使用线性反馈移位寄存器(LFSR)来生成数字。这有几个优点:首先,LFSR可能比大多数PRNG快一点。其次,(我相信)设计一个产生接近你想要的范围的数字的LFSR会更容易一些,并且仍然可以确保它以(伪)随机顺序循环其范围内的数字,而不会重复。
没有花费大量时间在细节上,LFSR背后的数学已经被彻底研究过了。生成一个贯穿其范围内所有数字而不重复的数据只需要选择一组对应于不可约多项式的“抽头”。如果您不想自己搜索,那么很容易找到几乎任何合理大小的已知表格(例如,快速查看,维基百科文章列出它们的大小最多为19位)。
如果存储器服务,则至少有一个不可能的比特大小的不可约多项式。这意味着在最坏的情况下,您可以创建一个大约是您所需范围的两倍的生成器,因此平均而言,您(大致)丢弃您生成的每个其他数字。考虑到LFSR的速度,我猜你可以做到并且仍然保持相当可接受的速度。
答案 1 :(得分:9)
一种方法是
p
的素数N
,最好不要大得多。g
模p
的原始根,即1 < g < p
,g^k ≡ 1 (mod p)
k
当且仅当p-1
为多个g^k (mod p)
。k = 1, 2, ...
N
,忽略大于p
的值。对于每个素数φ(p-1)
,有p
原始的统一根,所以它有效。但是,找一个可能需要一段时间。一般来说,找到合适的素数要容易得多。
为了找到一个原始根,我对试验和错误一无所知,但通过适当选择素数φ(p-1)
可以增加快速查找的概率。
由于原始根的数量为r
,如果在1到p-1
的范围内随机选择(p-1)/φ(p-1)
,则在找到原始根之前的预期尝试次数为{ {1}}因此,应该选择p
以使φ(p-1)
相对较大,这意味着p-1
必须具有少量不同的素数除数(最好只有大除数),除了因子2)。
不是随机选择,也可以按顺序尝试2, 3, 5, 6, 7, 10, ...
是否是原始根,当然是跳过完美的权力(或者不是,它们通常很快被消除),这不应该影响尝试次数非常需要。
因此,归结为检查数字x
是否是原始根模p
。如果p-1 = q^a * r^b * s^c * ...
具有不同的素数q, r, s, ...
,则x
是原始根,当且仅当
x^((p-1)/q) % p != 1
x^((p-1)/r) % p != 1
x^((p-1)/s) % p != 1
...
因此,需要一个适当的模幂运算(通过重复平方取幂,这很好地适用于此,通过每个步骤的模数减少)。并且找到p-1
的素因子分解的好方法。然而,请注意,即使是天真的试验除法也只是O(√p),而置换的生成是Θ(p),因此因子分解是最优的并不是最重要的。
答案 2 :(得分:4)
另一种方法是使用分组密码;有关详细信息,请参阅this blog post。
该博客发布了论文Ciphers with Arbitrary Finite Domains的链接,其中包含一系列解决方案。
答案 3 :(得分:2)
考虑素数3.要充分表达所有可能的输出,请以这种方式考虑......
bias + step mod prime
bias
只是偏移偏差。 step
是一个累加器(例如,如果它是1
,它将依次为0, 1, 2
,而2
将导致0, 2, 4
)和{{1}是我们想要生成排列的素数。
例如。一个简单的prime
序列就是......
0, 1, 2
修改其中的几个变量,我们将0 + 0 mod 3 = 0
0 + 1 mod 3 = 1
0 + 2 mod 3 = 2
bias
和1
step
(仅用于说明)...
2
你会注意到我们制作了完全不同的序列。集合中没有数字重复,所有数字都表示(它是双射的)。偏移和偏差的每个唯一组合将导致该组的1 + 2 mod 3 = 0
1 + 4 mod 3 = 2
1 + 6 mod 3 = 1
个可能的排列之一。对于prime!
prime
,您会看到3
种不同的可能性:
6
如果你对上面的变量进行数学计算,那么它不会产生相同的信息要求......
0,1,2
0,2,1
1,0,2
1,2,0
2,0,1
2,1,0
...... vs ...
1/3! = 1/6 = 1.66..
限制很简单,1/3 (bias) * 1/2 (step) => 1/6 = 1.66..
必须在bias
之内且0..P-1
必须在step
之内(我在功能上一直在使用1..P-1
并添加{ {1}}关于我自己的工作中的算术)。除此之外,它适用于所有质数,无论多大,都会排列所有可能的唯一集合,而不需要超过几个整数的内存(每个整数在技术上要求的位数比素数本身略少)。
请注意小心,此生成器不应用于生成非数字的集合。完全有可能这样做,但不建议用于安全性敏感的目的,因为它会引入定时攻击。
那就是说,如果你想用这个方法生成一个不是素数的集合序列,你有两个选择。
首先(和最简单/最便宜的),选择比您正在寻找的设定尺寸更大的素数,并让您的发电机简单地丢弃任何不属于的东西。再一次,危险,如果这是一个安全敏感的应用程序,这是一个非常糟糕的主意。
其次(迄今为止最复杂和最昂贵的),您可以认识到所有数字都由素数组成,并创建多个生成器,然后为集合中的每个元素生成产品。换句话说,0..P-2
1
会涉及所有可能匹配n
的素数生成器(在本例中为6
和6
),乘以序列。这既昂贵(虽然在数学上更优雅),同时也引入了定时攻击,所以更不推荐。
最后,如果您需要2
或3
的生成器...为什么不使用同一系列中的另一个:)。突然间,你非常接近于创建真正的简单随机样本(通常不容易)。
答案 4 :(得分:2)
LCG(x=(x*m+c)%b
样式生成器)的根本弱点在这里很有用。
如果生成器正确形成,则x%f
也是低于f
的所有值的重复序列(如果因子为f
,则提供b
)。
由于b
通常是2的幂,这意味着您可以通过屏蔽顶部位来使用32位发生器并将其减少为n位发生器,并且它将具有相同的全范围属性。
这意味着您可以通过选择适当的掩码将丢弃值的数量减少到少于N.
不幸的是,LCG是一个糟糕的发电机,原因与上面给出的完全相同。
此外,这与我在@JerryCoffin的回答评论中提到的完全一样的弱点。它将始终产生相同的序列,种子控制的唯一内容是从该序列开始的位置。
答案 5 :(得分:0)
这里有一些SageMath代码应该按Daniel Fischer suggested的方式生成随机排列:
def random_safe_prime(lbound):
while True:
q = random_prime(lbound, lbound=lbound // 2)
p = 2 * q + 1
if is_prime(p):
return p, q
def random_permutation(n):
p, q = random_safe_prime(n + 2)
while True:
r = randint(2, p - 1)
if pow(r, 2, p) != 1 and pow(r, q, p) != 1:
i = 1
while True:
x = pow(r, i, p)
if x == 1:
return
if 0 <= x - 2 < n:
yield x - 2
i += 1