请记住,我在这里描述的算法是基于列表 [1, 2, ... N-1] (长度为 N-1)。如果您坚持使用列表 [0, 1, ..., N](长度为 N+1),请应用所需的小修改。此外,为简洁起见,我使用 % 操作数的方式与大多数编程语言略有不同:a % b 取值介于 1 和 b 之间,而不是介于 0 和 b - 1 之间,但背后的主要思想当然没有改变, 所以a % b 的值是1和b之间的整数,与a一致,取模b。
如果你通读了这篇文章,你会很明显,生成的 shuffle 根本不是随机的。然而,如果参数选择得当,模式将不容易识别,(我的模幂运算的基本思想来自密码学,其中具有不可识别的模式和不可恢复的函数很重要)。
这更像是算法的语言无关描述,而不是实际的编程解决方案。我不会详细介绍您可能遇到的有效实现和陷阱。我希望它仍然有帮助。我还在 python 中编写了其中的一些部分,因此我可以提供进一步的帮助,甚至在需要时分享我的代码,但这之前需要一些完成和重构。
使用求幂而不是乘法来摆脱模式
您对 f(x) = t * x % N(您选择 t 为 911)的初步试验显示了一些模式,因为乘法保持线性(在它的“模块化”含义中)。
如果你使用指数而不是乘法,你可以给人更多随机的感觉。像 f(x) = t ^ x % N 之类的东西。但是,必须明智地选择 t(就像在乘法的情况下一样,与 N 互质),并且该公式给出的输出不会提供不同的仅在 N 为素数的情况下,不同 x 值的数字。
大学水平的数学即将到来,请耐心等待,我会尽量保持简单。
我们将需要使用原始根。相关的Wikipedia 文章可能有很大帮助,但基本思想是精心选择的基数的余数取 1 到 N-1 之间的所有值。例如
3^1 = 3
3^2 = 9 = 2 (mod 7)
3^3 = 27 = 6 (mod 7)
3^4 = 81 = 4 (mod 7)
3^5 = 243 = 5 (mod 7)
3^6 = 729 = 1 (mod 7)
都是不同的(从这一点开始,值从头开始重复:3^7 = 3^1 (mod 7)、3^8 = 3^2 (mod 7),依此类推)。
所以,如果你的 N 是 7,那么 3 就可以成为 t。您可以将 f(x) = (3 ^ x) % 7 用于 1 到 6 之间的 x 值。
这导致以下 f:
f(1) = 3
f(2) = 2
f(3) = 6
f(4) = 4
f(5) = 5
f(6) = 1
引入移位,提供一些额外的随机效果
如果你稍微玩一下,你会注意到,N-1 总是被洗牌为 1。如果你想避免这种情况,我们可以更进一步,选择一个任意数 k 在求幂后添加。也就是说,使用 g(x) = (f(x) + k) % (N-1) = ((t ^ x) % N + k) % (N-1),在我们的示例中让 k 为 2,导致排列:
g(1) = 5
g(2) = 4
g(3) = 2
g(4) = 6
g(5) = 1
g(6) = 3
如何选择底座
现在你得到了基本的感觉。但是一般如何使用这个,当N不是7时?
问题的关键是选择参数 t,在我们的示例中为 3。
不幸的是,这通常是一个难题(数学家称之为寻找原始根),而且我知道没有任何易于解释、开箱即用的解决方案。
但这只是问题的一部分。更可悲的是,如果 N 是合数,则原根甚至都不起作用。例如,如果 N=6,则表达式 t^x 以 6 取 1 到 5 之间的所有值都不存在任何数字 t。
但这部分解决起来并不难。
如何处理复合 N
如果 N 是复合的,我们应该找到一个稍大一点的素数 P,并在基于该算法的算法的基础上,通过用它们的洗牌后值替换超出范围的数字(如果需要,迭代) .
例如,如果 N 为 6,我们可以选择 P 为 7 并使用我们之前构建的 g(x)。
g(1) = 5 ok (5<=N-1 holds) h(1) = 5
g(2) = 4 ok h(2) = 4
g(3) = 2 ok => h(3) = 2
g(4) = 6 too large, using g(g(4)) = g(6) = 3 h(4) = 3
g(5) = 1 ok h(5) = 1
为了安全起见,我举了另一个 N=4 的例子,我们使用之前计算的 P=7 的解决方案。
g(1) = 5, g(5) = 1 h(1) = 1
g(2) = 4, g(4) = 6, g(6) = 3 => h(2) = 3
g(3) = 2 h(3) = 2
现在应该很清楚了。选择接近 N 的 P 是明智的,因此对于 g 的越界值不需要太多的重新计算。
回到寻找 t
所以我们剩下的唯一问题是找到可以用作求幂基础的原根。
如果我之前链接的页面上的数学引起了一些内心的厌恶,我有一些好消息要告诉你:t 的可能好的值在区间 [2, N-1] 中是密集的,所以即使是随机猜测也可能会有所帮助。
有一些详细信息如何有效地验证随机选择的 t 在链接页面上是否真的对我们有好处,但是除非您使用非常大的数字,否则您可以只进行求幂并检查数字 1 是否早于 ( t 的 N-1) 次方(也许你记得我注意到 t^x=1 (mod N) 在 x=N-1 的情况下始终成立,因此 1 的早期出现会破坏唯一性)。
我建议在 N/2 附近寻找合适的 t(意味着数量级 - 对于 P=91367,t=54949 效果很好)。如果您选择 t 太低(例如 t=2),您可以很容易地发现一些相邻 x 值(2+k, 4+k, 8+k, ... 会出现在彼此)。如果 t 太接近 N,如果在相同奇偶性的连续 x 值中查看 f(x),可能会出现类似的现象。一个好的 t 选择应该涵盖这些模式,并以足够随机的结果结束。
概括
所以再一次,这里是算法的步骤
(N 给定)
找到一个比 N 稍大的 P 素数
选择 1 到 P-1 之间的任意数字 k
找到 t,它是 P 的原根
(对于给定的 x,输出 shuffle h(x) 是)
计算
f(x) = (t ^ x) % P
计算
g(x) = (f(x) + k) % (P-1)
计算
h(x) = g(x) if g(x)<=N-1,
iterate the calculations with x = g(x) otherwise